From 116aacba3ccdb060b9ad79fdbd1cf916efc45902 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 29 Oct 2026 03:36:32 +0000 Subject: [PATCH] test: pin the own-policy fix line's guards, its user-policy source and receipt duplicates A head-policy run and a branch deleting pregate.json get the plain fix; under a user-owned policy the line names the user file; a body with two receipt blocks refuses like a doubled title. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- test/gstack-post.test.ts | 8 ++++++++ test/pregate-selection.test.ts | 19 +++++++++++++++++++ test/pregate-user-policy.test.ts | 13 +++++++++++++ 3 files changed, 40 insertions(+) diff --git a/test/gstack-post.test.ts b/test/gstack-post.test.ts index 3ff3890fe..fc952a844 100644 --- a/test/gstack-post.test.ts +++ b/test/gstack-post.test.ts @@ -554,6 +554,14 @@ describe("gstack-post pr-body merges the PR body by section", () => { expect(sent(replaced).input).toBe(fresh); }); + // Value: protects=a current body holding two ship-receipt blocks (an old receipt pasted below) refuses like a doubled title, so the stale + // receipt is never kept beside the new one; fails_when=receipt keys are exempted from the duplicate check or matched one to one again; + // why_new=only doubled ## titles were tested; seam=none; no_claim=how /ship reports the refusal + test("a current body with two receipt blocks refuses a file that carries a receipt", () => { + const current = RECEIPT("aaaa") + "\n## A\n\na\n\n" + RECEIPT("bbbb") + "\n## B\n\nb\n"; + expect(() => mergeBodySections(current, RECEIPT("cccc") + "\n## A\n\nnew\n")).toThrow('holds "ship receipt" more than once'); + }); + // Value: protects=the body /ship composes (lib/ship-receipt.ts placeReceiptFirst: the receipt block, one blank line, then the sections) reposts // idempotently: merged into a PR body of the same shape it yields the file's bytes, the receipt stays byte-first, and the stderr line reports no // addition; fails_when=the blank line after the receipt is read as new leading text and prepended before the receipt (one more leading newline per diff --git a/test/pregate-selection.test.ts b/test/pregate-selection.test.ts index e83e61a60..489bb1982 100644 --- a/test/pregate-selection.test.ts +++ b/test/pregate-selection.test.ts @@ -515,6 +515,25 @@ describe('gstack-pregate (bin)', () => { expect(fix).toContain('otherwise add a test that names the file'); }); + // Value: protects=the own-policy fix line appears only when the base or user copy decides and the branch still carries its own + // pregate.json: a --policy-from head run and a branch that deletes the file get the plain fix; fails_when=the configKind 'head' guard or + // the status 'D' filter in noLaneFix is dropped; why_new=the head run above passes and never reaches noLaneFix, and no test deletes the + // pin file on a NO_LANE run; seam=fixture repo; no_claim=the wording of the plain fix + test('lanes: a head-policy run and a branch that deletes pregate.json get the plain no-lane fix', () => { + const repo = fixture(); + commit(repo, { 'lib/orphan.js': "exports.lonely = () => 2;\n", '.gstack/pregate.json': JSON.stringify({ ...BASE_PREGATE, dependencies: { ...BASE_PREGATE.dependencies, 'lib/orphan.js': ['test/util.test.js'] } }) + '\n' }); + commit(repo, { 'lib/orphan2.js': "exports.alone = () => 3;\n" }); + const head = pregate(repo, ['--stage', 'tests', '--policy-from', 'head']); + expect(head.status).toBe(EXIT.fail); + expect(head.err).toContain('(PREGATE_NO_LANE)'); + expect(check(repo, 'lanes').fix).toMatch(/^add a test that names the file/); + commit(repo, { '.gstack/pregate.json': null }); + const deleted = pregate(repo, ['--stage', 'tests']); + expect(deleted.status).toBe(EXIT.fail); + expect(deleted.err).toContain('(PREGATE_NO_LANE)'); + expect(check(repo, 'lanes').fix).toMatch(/^add a test that names the file/); + }); + test('lanes: deleted and renamed files select the tests that named them; subprocess targets and computed reads map through declarations and joined literals', () => { const repo = fixture(); repo.git('mv', 'lib/util.js', 'lib/maths.js'); diff --git a/test/pregate-user-policy.test.ts b/test/pregate-user-policy.test.ts index b56a32032..1cd0646fa 100644 --- a/test/pregate-user-policy.test.ts +++ b/test/pregate-user-policy.test.ts @@ -131,6 +131,19 @@ describe('gstack-pregate with a user-owned policy', () => { expect(lanes.fix).toContain(`under dependencies — or empty_lanes for other documentation — in ${userPolicy}`); }, 60_000); + // Value: protects=under a user-owned policy, a branch that adds its own .gstack/pregate.json is told the user file is the policy, by path; + // fails_when=the own-policy line is limited to the base copy or hardcodes origin as the source; why_new=the own-policy line is new and its + // test covers the base copy only; seam=none; no_claim=which route suits a user-policy repository + test('the own-policy no-lane fix names the user file as the policy source', () => { + const { repo, userPolicy, run } = setup(); + writePolicy(userPolicy, { local: LOCAL_ALL }); + commit(repo, { 'lib/orphan.js': 'exports.lonely = () => 2;\n', '.gstack/pregate.json': JSON.stringify({ runner: 'node --test {files}', dependencies: { 'lib/orphan.js': ['test/util.test.js'] } }) }); + expect(run().status).not.toBe(EXIT.ok); + const lanes = JSON.parse(fs.readFileSync(path.join(repo.work, '.gstack/tmp/pregate.json'), 'utf8')).checks.find((c: { id: string }) => c.id === 'lanes'); + expect(lanes.code).toBe('PREGATE_NO_LANE'); + expect(lanes.fix).toStartWith(`this branch edits .gstack/pregate.json, but the policy comes from user ${userPolicy} so a branch cannot loosen its own gate`); + }, 60_000); + // Value: protects=an invalid, sealed-bearing or unreadable user file refuses with PREGATE_CONFIG_INVALID naming the file, with a fix // that points at the file rather than the base branch; fails_when=user-file errors run on defaults, or sealed/price_file are accepted // and look active while nothing reads them; why_new=the user source is new; seam=none; no_claim=the sealed guard itself