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:
garrytanandcapy-ai[bot] committed 2026-11-03 09:29:43 +00:00
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);
});