mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
test: failing-first carry cases for stamp-file reverts and ported harness pins (GST-1210)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
8d18303212
commit
0b36079b2a
2 files changed
+178
No files matched your search
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* gstack-ship-receipt carry across a changelog-fragment conversion (GST-1210, HARN-1 harness-evals#206): a PR reviewed
|
||||
* with its own version stamp (VERSION, package.json, CHANGELOG) moves to a changelog.d fragment after the base adopts
|
||||
* fragments, which puts every stamp file back at the base's content. A stamp file the branch no longer changes against
|
||||
* the base, or changes only in its stamp fields, holds no unreviewed change, so the receipt carries; a non-stamp change
|
||||
* in package.json or a logic edit next to the reviewed one still refuses.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { extractReceipt } from '../lib/ship-receipt';
|
||||
import { cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
|
||||
const repos: FixtureRepo[] = [];
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
const receiptIn = (body: string) => extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
|
||||
const carry = (repo: FixtureRepo, body: string) => runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
||||
const pkg = (version: string, scripts: Record<string, string> = {}) => JSON.stringify({ name: 'fixture', version, scripts: { test: 'node --test test/*.test.js', generate: 'node gen.js', ...scripts } }, null, 2) + '\n';
|
||||
const POLICY = (fragments: boolean) => JSON.stringify({ min_gstack_version: '1.91.340.0', stamp_files: ['VERSION', 'package.json', 'CHANGELOG.md'], ...(fragments ? { changelog_fragments: { dir: 'changelog.d' } } : {}) }, null, 2) + '\n';
|
||||
const RETRY4 = "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n";
|
||||
|
||||
/**
|
||||
* main at 1.0.0 <- feature: four retries plus its own 1.0.1 stamp, /ship receipt written there. main then ships 1.0.3,
|
||||
* adopts changelog fragments and (`mainScripts`) changes package.json beyond its version. The branch merges main and
|
||||
* converts: `converted` is the package.json it keeps (main's by default), plus a fragment and any `extra` files.
|
||||
*/
|
||||
function converted(repo: FixtureRepo, opts: { mainScripts?: Record<string, string>; keep?: (main: string) => string; extra?: Record<string, string> } = {}): { reviewed: string; body: string } {
|
||||
repo.git('checkout', '-q', 'main');
|
||||
commit(repo, { VERSION: '1.0.0\n', 'CHANGELOG.md': '# Changelog\n\n## 1.0.0\n- base\n', '.gstack/ship-policy.json': POLICY(false) }, 'main: stamp files');
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
repo.git('checkout', '-q', 'feature');
|
||||
repo.git('reset', '-q', '--hard', 'origin/main');
|
||||
const reviewed = commit(repo, { 'lib/retry.js': RETRY4, VERSION: '1.0.1\n', 'package.json': pkg('1.0.1'), 'CHANGELOG.md': '# Changelog\n\n## 1.0.1\n- four retries\n\n## 1.0.0\n- base\n' }, 'feat: four retries (v1.0.1)');
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '206', '--base', 'main', '--version', '1.0.1', '--session-kind', 'unattended', '--body', body]);
|
||||
expect(w.status).toBe(EXIT.ok);
|
||||
repo.git('checkout', '-q', 'main');
|
||||
const mainPkg = pkg('1.0.3', opts.mainScripts);
|
||||
commit(repo, { VERSION: '1.0.3\n', 'package.json': mainPkg, 'CHANGELOG.md': '# Changelog\n\n## 1.0.3\n- other PRs\n\n## 1.0.0\n- base\n', '.gstack/ship-policy.json': POLICY(true), 'changelog.d/README.md': '# fragments\n' }, 'main: v1.0.3, changelog fragments');
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
repo.git('checkout', '-q', 'feature');
|
||||
try { repo.git('merge', '-q', '--no-edit', 'origin/main'); } catch { /* the stamp files conflict; the conversion resolves them below */ }
|
||||
repo.git('checkout', 'origin/main', '--', 'VERSION', 'CHANGELOG.md', 'package.json');
|
||||
write(repo.work, 'package.json', (opts.keep ?? (m => m))(mainPkg));
|
||||
write(repo.work, 'changelog.d/four-retries.md', '- four retries\n');
|
||||
for (const [rel, text] of Object.entries(opts.extra ?? {})) write(repo.work, rel, text);
|
||||
repo.git('add', '-A');
|
||||
repo.git('commit', '-q', '--no-edit', '-m', 'convert to a changelog fragment');
|
||||
return { reviewed, body };
|
||||
}
|
||||
|
||||
describe('carry across a changelog-fragment conversion (GST-1210)', () => {
|
||||
// Value: protects=a PR converted to a changelog fragment after review keeps its receipt when the base also changed
|
||||
// package.json beyond its version (HARN-1 harness-evals#206 paid a fresh /ship); fails_when=carry reads the base's own
|
||||
// package.json change as the branch's product change; seam=none; no_claim=gh pr edit (--body stands in)
|
||||
test('a stamp file back at the base\'s content carries as release metadata', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { reviewed, body } = converted(repo, { mainScripts: { lint: 'node lint.js' } });
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const out = carry(repo, body);
|
||||
expect(out.err).not.toContain('RECEIPT_CARRY');
|
||||
expect(out.status).toBe(EXIT.ok);
|
||||
expect(out.out).toContain(`CARRY: carried from ${reviewed.slice(0, 12)} to ${head.slice(0, 12)}`);
|
||||
expect(receiptIn(body).carry!.reason).toContain('release metadata');
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=the converted PR whose package.json is the base's except its version field carries as stamp-only;
|
||||
// fails_when=the version difference against the base reads as a product change; seam=none; no_claim=gh pr edit
|
||||
test('a package.json equal to the base\'s except the version field carries', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { body } = converted(repo, { mainScripts: { lint: 'node lint.js' }, keep: m => m.replace('"version": "1.0.3"', '"version": "1.0.4"') });
|
||||
const out = carry(repo, body);
|
||||
expect(out.err).not.toContain('RECEIPT_CARRY');
|
||||
expect(out.status).toBe(EXIT.ok);
|
||||
expect(receiptIn(body).carry!.reason).toContain('release metadata');
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=fail closed on a package.json field the branch changed beyond the stamp (a new script runs code);
|
||||
// fails_when=a stamp file that is not the base's content is read as stamp-only; seam=none; no_claim=gh pr edit
|
||||
test('a non-stamp field the branch adds to package.json still refuses', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { body } = converted(repo, { mainScripts: { lint: 'node lint.js' }, keep: m => m.replace('"lint": "node lint.js"', '"lint": "node lint.js",\n "postinstall": "node evil.js"') });
|
||||
const out = carry(repo, body);
|
||||
expect(out.status).toBe(EXIT.refused);
|
||||
expect(out.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
|
||||
expect(out.err).toContain('package.json');
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=fail closed on a logic edit beside the reviewed one riding the conversion; fails_when=the stamp-file
|
||||
// rule widens to the whole diff; seam=none; no_claim=gh pr edit
|
||||
test('a logic edit next to the reviewed constant still refuses', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { body } = converted(repo, { mainScripts: { lint: 'node lint.js' }, extra: { 'lib/retry.js': RETRY4.replace('TIMEOUT_MS = 5000', 'TIMEOUT_MS = 50') } });
|
||||
const out = carry(repo, body);
|
||||
expect(out.status).toBe(EXIT.refused);
|
||||
expect(out.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
|
||||
expect(out.err).toContain('lib/retry.js');
|
||||
}, 90_000);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* gstack-ship-receipt carry across a base merge that ports one harness-pin entry (GST-1210, GSTA-202 #3903 at f0613db8):
|
||||
* the branch splits the base's JSON map of pins into one file per entry, and a later merge from the base copies the base's
|
||||
* edit of one entry into its pin file, byte for byte. That file holds the base's change and nothing of the branch's, so
|
||||
* the receipt carries; a pin that differs from the base's entry, or lands in another entry's file, still refuses.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { extractReceipt } from '../lib/ship-receipt';
|
||||
import { cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
|
||||
|
||||
const repos: FixtureRepo[] = [];
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
const receiptIn = (body: string) => extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
|
||||
const carry = (repo: FixtureRepo, body: string) => runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
||||
const RETRY = '{"blob":"aaa","since":6,"nonBehavioral":"first pin"}';
|
||||
const UTIL = '{"blob":"bbb","since":6}';
|
||||
const RETRY_MAIN = '{"blob":"ccc","since":6,"nonBehavioral":"main repinned retry"}';
|
||||
const manifest = (retry: string) => `{\n "version": 6,\n "files": {\n "lib/retry.js": ${retry},\n "lib/util.js": ${UTIL}\n }\n}\n`;
|
||||
const PIN = 'scripts/harness-pins/lib/retry.js.json';
|
||||
|
||||
/** main holds the pin map; feature splits it into one file per entry beside a product change, /ship receipt there; main repins retry; feature merges main and writes `ports`. */
|
||||
function ported(repo: FixtureRepo, ports: Record<string, string>): { reviewed: string; body: string } {
|
||||
repo.git('checkout', '-q', 'main');
|
||||
commit(repo, { 'scripts/harness-version.json': manifest(RETRY) }, 'main: harness pins');
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
repo.git('checkout', '-q', 'feature');
|
||||
repo.git('reset', '-q', '--hard', 'origin/main');
|
||||
const reviewed = commit(repo, { 'lib/retry.js': "exports.MAX_RETRIES = 4;\nexports.TIMEOUT_MS = 5000;\nexports.retry = (fn) => fn();\n", [PIN]: `${RETRY}\n`, 'scripts/harness-pins/lib/util.js.json': `${UTIL}\n` }, 'feat: one file per harness pin');
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
expect(runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3903', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body]).status).toBe(EXIT.ok);
|
||||
repo.git('checkout', '-q', 'main');
|
||||
commit(repo, { 'scripts/harness-version.json': manifest(RETRY_MAIN), 'docs/README.md': '# docs after the repin\n' }, 'main: repin retry');
|
||||
repo.git('push', '-q', 'origin', 'main');
|
||||
repo.git('checkout', '-q', 'feature');
|
||||
repo.git('merge', '-q', '--no-edit', 'origin/main');
|
||||
commit(repo, ports, 'port the base repin into its pin file');
|
||||
return { reviewed, body };
|
||||
}
|
||||
|
||||
describe('carry across a ported harness-pin entry (GST-1210)', () => {
|
||||
// Value: protects=a branch that splits harness pins into one file per entry keeps its receipt when a base merge ports
|
||||
// the base's repin byte for byte (#3903 at f0613db8 was refused); fails_when=carry reads the ported pin as the branch's
|
||||
// product change; seam=none; no_claim=gh pr edit (--body stands in)
|
||||
test('a pin file equal to the base\'s entry byte for byte carries', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { reviewed, body } = ported(repo, { [PIN]: `${RETRY_MAIN}\n` });
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const out = carry(repo, body);
|
||||
expect(out.err).not.toContain('RECEIPT_CARRY');
|
||||
expect(out.status).toBe(EXIT.ok);
|
||||
expect(out.out).toContain(`CARRY: carried from ${reviewed.slice(0, 12)} to ${head.slice(0, 12)}`);
|
||||
expect(receiptIn(body).carry!.reason).toContain(`base entries ported byte for byte (${PIN})`);
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=fail closed on a pin the branch changed beyond the base's entry (an unreviewed repin);
|
||||
// fails_when=any pin file change carries; seam=none; no_claim=gh pr edit
|
||||
test('a pin entry that differs from the base\'s still refuses', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { body } = ported(repo, { [PIN]: `${RETRY_MAIN.replace('ccc', 'ddd')}\n` });
|
||||
const out = carry(repo, body);
|
||||
expect(out.status).toBe(EXIT.refused);
|
||||
expect(out.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
|
||||
expect(out.err).toContain(PIN);
|
||||
}, 90_000);
|
||||
|
||||
// Value: protects=fail closed when the base's entry lands in another entry's pin file; fails_when=the port matches the
|
||||
// value without its key; seam=none; no_claim=gh pr edit
|
||||
test('the base\'s entry written into another entry\'s pin file still refuses', () => {
|
||||
const repo = makeFixtureRepo(); repos.push(repo);
|
||||
const { body } = ported(repo, { [PIN]: `${RETRY_MAIN}\n`, 'scripts/harness-pins/lib/util.js.json': `${RETRY_MAIN}\n` });
|
||||
const out = carry(repo, body);
|
||||
expect(out.status).toBe(EXIT.refused);
|
||||
expect(out.err).toContain('scripts/harness-pins/lib/util.js.json');
|
||||
}, 90_000);
|
||||
});
|
||||
Reference in new issue
Block a user