mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
* Load the Capy crew skills only on Capy machines (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add changelog entry for Capy-only crew skills (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Move the Capy-only crew tooling into crew/ and run its tests only when needed (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Test that the Capy launch file puts crew/bin on PATH Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Count any ~/.capy as a Capy machine, so a Mac running Capy keeps /captain Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Fix the tests and fixtures that broke when crew tooling moved into crew/ Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Plan every crew test when a pull request's merge-base can't be read Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Add a test that checks each crew-only module left the root tree for crew/ Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Run crew tests on pull requests that change bin/ commands or workflows they drive Review-Finding: p1/specialist-batched#1 reader=testing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep running every crew test on every pull request; the skip comes in a later change Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Keep copied installs and older release bisects working after the crew/ move Setup now copies crew/lib into every host's runtime root beside bin and lib, so Windows installs (which copy instead of linking) can still start gstack-ship-receipt, gstack-pregate and the other commands that import it. The release bisect runs the train-gate script from wherever the checked-out commit has it, so a bisect over commits from before the move still gets a verdict. Review-Finding: p1/outside-voice#1 reader=outside-voice Review-Finding: p1/outside-voice#2 reader=outside-voice Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Say in the changelog that Windows installs keep working after the crew/ move Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Point the harness guide's merge-queue rows at crew/lib/ where those modules moved Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Point the plain-title test at the wave module's new home in crew/ (GST-1833) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
325 lines
24 KiB
TypeScript
325 lines
24 KiB
TypeScript
/**
|
|
* The /ship receipt's review depth and review-loop exception (GSTA-147 PR 2,
|
|
* docs/review-policy.md#review-depth, #3559): write records the depth gstack
|
|
* computes at the checked-out head, the queue recomputes the depth the head
|
|
* needs and refuses a receipt below it, a carry rebinds the gate scan or
|
|
* refuses when the head needs more, and the review-loop exception is checked
|
|
* like the documentation exception.
|
|
*/
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { EXIT } from '../lib/headless-artifacts';
|
|
import { extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
|
|
import { bindShippedReceipt, receiptDepthAtHead, receiptDepthVerdict, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
|
|
import { bunIsolationArgs } from '../lib/bun-isolation';
|
|
import { runScriptSync } from '../lib/script-invocation';
|
|
import { BUMP_NEVER_KEY_VERSION, POLICY_KEY_INTRODUCED, TRAIN_GATES_KEY_VERSION } from '../lib/ship-policy';
|
|
import { cmpVersion, parseVersion } from '../lib/version-source';
|
|
import { cleanup, commit, makeFixtureRepo, ROOT, 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 writeReceipt = (repo: FixtureRepo, body: string, extra: string[] = []) =>
|
|
runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body, ...extra]);
|
|
/** A done targeted record read at HEAD on every class: since GST-986, write refuses a money, gate, auth or secret head without one. */
|
|
const targetedAtHead = (repo: FixtureRepo) => ['--review-targeted', JSON.stringify({ classes: ['money', 'gate', 'auth', 'secret'], rules: 'r', brief_sha: 'a'.repeat(64), status: 'done', findings: 0, raised_by: 'targeted', head: repo.git('rev-parse', 'HEAD') })];
|
|
const lines = (n: number) => Array.from({ length: n }, (_, i) => `exports.f${i} = () => ${i};`).join('\n') + '\n';
|
|
const rewrite = (body: string, edit: (r: Record<string, any>) => void) => {
|
|
const text = fs.readFileSync(body, 'utf8');
|
|
const r = extractReceipt(text).receipt as Record<string, any>;
|
|
edit(r);
|
|
fs.writeFileSync(body, text.replace(/```gstack-ship-receipt[\s\S]*?\n```\n/, renderReceiptBlock(r as any)));
|
|
return fs.readFileSync(body, 'utf8');
|
|
};
|
|
|
|
describe('write records the review depth at the checked-out head', () => {
|
|
// Value: protects=the receipt names the depth and a gate scan bound to the head it is written for, and never binds a
|
|
// tree it did not review; fails_when=write records no depth, or --head accepts a commit other than HEAD; seam=none
|
|
test('a light code diff records depth, runtime lines, the base and the gate scan at HEAD; --head of another commit writes nothing', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const older = commit(repo, { 'lib/util.js': lines(3) });
|
|
const head = commit(repo, { 'lib/more.js': lines(2) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
const ok = writeReceipt(repo, body);
|
|
expect(ok.status).toBe(EXIT.ok);
|
|
expect(ok.err).toContain('depth=light');
|
|
const r = receiptIn(body);
|
|
expect(r.gstack_version).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
|
|
const base = repo.git('rev-parse', 'origin/main');
|
|
expect(r.review_depth).toMatchObject({ depth: 'light', class: 'code', base, registry: base, gate_scan: { tagged: 0, head } });
|
|
expect(r.review_depth!.runtime_lines).toBeGreaterThanOrEqual(5);
|
|
expect(r.review_depth!.runtime_lines).toBeLessThanOrEqual(50);
|
|
expect(r.design).toBeUndefined();
|
|
const other = path.join(repo.root, 'other.md');
|
|
const bad = writeReceipt(repo, other, ['--head', older]);
|
|
expect(bad.status).toBe(EXIT.fail);
|
|
expect(bad.err).toContain(`--head ${older.slice(0, 12)} is not the checked-out HEAD ${head.slice(0, 12)}`);
|
|
expect(bad.err).toContain('(RECEIPT_INVALID)');
|
|
expect(fs.existsSync(other)).toBe(false);
|
|
}, 60_000);
|
|
|
|
// Value: protects=a rendered-UI diff's receipt says only the static design check ran and why (GST-1718), from the same
|
|
// diff-scope the depth reads; fails_when=write records no design field on a frontend range, or records one on a
|
|
// backend-only range (test above); seam=none
|
|
test('a template-UI diff records design: static (no preview declared)', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
commit(repo, { 'src/ui/row.ts': "import { html } from 'lit';\nexport const row = (n: string) => html`<div class=\"row\">${n}</div>`;\n" });
|
|
const body = path.join(repo.root, 'body.md');
|
|
const ok = writeReceipt(repo, body);
|
|
expect(ok.status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).design).toEqual({ mode: 'static', reason: 'no preview declared' });
|
|
}, 60_000);
|
|
|
|
// Value: protects=a receipt names the gstack release that wrote it, never a newer policy key its tree carries, so a
|
|
// version check against a receipt cannot be passed by an older build (GST-1366); fails_when=write records the
|
|
// key-inflated version policy reading uses, or policy reading stops seeing the newest key; seam=none
|
|
test('a gstack whose VERSION is older than its newest policy key records that VERSION; policy reading still reads at the key', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
commit(repo, { 'lib/util.js': lines(3) });
|
|
const installed = path.join(repo.root, 'installed-gstack');
|
|
const sourceOnly = { recursive: true, filter: (file: string) => path.basename(file) !== 'node_modules' };
|
|
for (const dir of ['bin', 'lib', 'scripts', 'crew']) fs.cpSync(path.join(ROOT, dir), path.join(installed, dir), sourceOnly);
|
|
fs.writeFileSync(path.join(installed, 'VERSION'), '1.0.0.0\n');
|
|
const body = path.join(repo.root, 'body.md');
|
|
const write = runScriptSync(path.join(installed, 'bin', 'gstack-ship-receipt'), ['write', '--pr', '7', '--base', 'main', '--version', '1.0.0', '--session-kind', 'unattended', '--body', body],
|
|
{ cwd: repo.work, encoding: 'utf8', timeout: 180_000, maxBuffer: 64 * 1024 * 1024, env: { ...process.env, ...repo.env } });
|
|
expect(write.status, write.stderr).toBe(EXIT.ok);
|
|
expect(receiptIn(body).gstack_version).toBe('1.0.0.0');
|
|
const newestKey = [...Object.values(POLICY_KEY_INTRODUCED), BUMP_NEVER_KEY_VERSION, TRAIN_GATES_KEY_VERSION].reduce((a, b) => (cmpVersion(parseVersion(b)!, parseVersion(a)!) > 0 ? b : a));
|
|
const reader = spawnSync(process.execPath, [...bunIsolationArgs(), '-e', `console.log((await import(${JSON.stringify(path.join(installed, 'lib', 'ship-policy.ts'))})).runningGstackVersion())`],
|
|
{ cwd: repo.work, encoding: 'utf8', timeout: 60_000 });
|
|
expect(reader.stdout.trim(), reader.stderr).toBe(newestKey);
|
|
}, 120_000);
|
|
});
|
|
|
|
describe('the queue refuses a receipt below the depth its head needs', () => {
|
|
// Value: protects=a receipt reviewed lighter than its head needs never READYs or seats, and the refusal says what to do;
|
|
// fails_when=the queue trusts the recorded depth, or prints no required depth, reasons and inputs; seam=bindShippedReceipt
|
|
// R51: size never raises the depth, so the head that needs more than light is a risk-class one (a money path needs full).
|
|
test('standard on a full (money) head, a missing depth from a current writer and an unbound gate scan refuse; a legacy receipt is accepted', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const head = commit(repo, { 'lib/billing.js': lines(80) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
expect(writeReceipt(repo, body, targetedAtHead(repo)).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', runtime_lines: 80, risk: ['money'] });
|
|
const base = repo.git('rev-parse', 'origin/main').slice(0, 12);
|
|
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
|
|
const untargeted = bind(rewrite(body, r => { delete r.review_targeted; }));
|
|
expect(!untargeted.ok && untargeted.code).toBe('QUEUE_RECEIPT_TARGETED_MISSING');
|
|
expect(!untargeted.ok && untargeted.depthLine).toContain('REVIEW_DEPTH: receipt=full required=full');
|
|
const good = bind(rewrite(body, r => { r.review_targeted = { classes: ['money'], rules: 'r', brief_sha: 'a'.repeat(64), product_patch_id: r.product_patch_id, status: 'done', findings: 0, raised_by: 'targeted' }; }));
|
|
expect(good.ok, !good.ok ? good.message : '').toBe(true);
|
|
expect(good.ok && good.depthLine).toContain('REVIEW_DEPTH: receipt=full required=full runtime_lines=80 class=code');
|
|
|
|
const low = bind(rewrite(body, r => { r.review_depth.depth = 'standard'; }));
|
|
expect(low.ok).toBe(false);
|
|
if (low.ok) return;
|
|
expect(low.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
|
|
expect(low.depthLine).toContain('receipt=standard required=full runtime_lines=80');
|
|
expect(low.depthLine).toContain(`inputs=base@${base},registry@${base},gate_scan@${head.slice(0, 12)}`);
|
|
expect(low.message).toContain('risk:money');
|
|
expect(low.message).toContain(`the receipt is unchanged; run /ship on ${head.slice(0, 12)}`);
|
|
|
|
const unbound = bind(rewrite(body, r => { r.review_depth.depth = 'standard'; r.review_depth.gate_scan.head = 'a'.repeat(40); }));
|
|
expect(unbound.ok).toBe(false);
|
|
expect(!unbound.ok && unbound.depthLine).toContain('required=full');
|
|
expect(!unbound.ok && unbound.message).toContain('gate_scan_unavailable');
|
|
|
|
const none = bind(rewrite(body, r => { delete r.review_depth; }));
|
|
expect(!none.ok && none.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
|
|
expect(!none.ok && none.message).toContain('records no review_depth though its writer records one');
|
|
|
|
const legacy = rewrite(body, r => { delete r.gstack_version; });
|
|
const verdict = receiptDepthVerdict(repo.work, 'origin/main', extractReceipt(legacy).receipt!, head);
|
|
expect(verdict).toEqual({ ok: true, line: 'REVIEW_DEPTH: legacy receipt (written before receipts recorded review_depth); accepted' });
|
|
}, 60_000);
|
|
|
|
// Value: protects=the queue's required depth keeps the scope signals the writer used, and a gate scan binds only the full
|
|
// bound commit; fails_when=the queue recomputes without diff-scope (auth reads light) or binds an empty or short scan head
|
|
// by prefix; seam=bindShippedReceipt
|
|
test('an auth path needs full at the queue though the receipt says light; an empty or short gate scan head binds nothing', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const head = commit(repo, { 'lib/auth.js': lines(3) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
expect(writeReceipt(repo, body, targetedAtHead(repo)).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'full', class: 'gate' });
|
|
const bind = (text: string) => bindShippedReceipt(repo.work, 'origin/main', text, '7', [head]);
|
|
const light = bind(rewrite(body, r => { r.review_depth.depth = 'light'; }));
|
|
expect(!light.ok && light.code).toBe('QUEUE_RECEIPT_DEPTH_RAISED');
|
|
expect(!light.ok && light.depthLine).toContain('receipt=light required=full');
|
|
expect(!light.ok && light.depthLine).toContain('class=gate');
|
|
|
|
const plain = makeFixtureRepo(); repos.push(plain);
|
|
const plainHead = commit(plain, { 'lib/util.js': lines(3) });
|
|
const plainBody = path.join(plain.root, 'body.md');
|
|
expect(writeReceipt(plain, plainBody).status).toBe(EXIT.ok);
|
|
for (const scanHead of ['', plainHead.slice(0, 12)]) {
|
|
const r = bindShippedReceipt(plain.work, 'origin/main', rewrite(plainBody, x => { x.review_depth.gate_scan.head = scanHead; }), '7', [plainHead]);
|
|
expect(r.ok).toBe(false);
|
|
expect(!r.ok && r.depthLine).toContain('required=full');
|
|
expect(!r.ok && r.depthLine).toContain('gate_scan@missing');
|
|
}
|
|
}, 90_000);
|
|
});
|
|
|
|
describe('carry and the review depth', () => {
|
|
// Value: protects=a carried receipt keeps a gate scan bound to the head it now names, and a carry whose new head needs a
|
|
// deeper review (a skipped test) is refused; fails_when=carry keeps the old scan, or carries a gate-tagged test change; seam=none
|
|
test('a plain test change carries and rebinds the gate scan; a skipped test raises the depth and refuses the carry', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
commit(repo, { 'lib/util.js': lines(3), 'test/util.test.js': "test('adds', () => { expect(1).toBe(1); });\n" });
|
|
const body = path.join(repo.root, 'body.md');
|
|
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_depth!.depth).toBe('light');
|
|
const plain = commit(repo, { 'test/util.test.js': "test('adds', () => { expect(1).toBe(1); });\ntest('more', () => { expect(2).toBe(2); });\n" });
|
|
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
|
expect(carried.status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_depth).toMatchObject({ depth: 'light', gate_scan: { tagged: 0, head: plain } });
|
|
const skipped = commit(repo, { 'test/util.test.js': "test.skip('adds', () => { expect(1).toBe(1); });\ntest('more', () => { expect(2).toBe(2); });\n" });
|
|
const refused = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
|
expect(refused.status).toBe(EXIT.refused);
|
|
expect(refused.out).toContain('CARRY: refused (RECEIPT_CARRY_REFUSED)');
|
|
expect(refused.err).toContain(`HEAD ${skipped.slice(0, 12)} needs full review depth`);
|
|
expect(refused.err).toContain("above the receipt's light");
|
|
expect(receiptIn(body).head).toBe(plain);
|
|
}, 90_000);
|
|
|
|
// Value: protects=a carry never binds a scan of one checkout to another commit, and never rebinds a receipt the queue would
|
|
// refuse; fails_when=carry --head from another checkout carries a skipped test at light, or a current receipt without
|
|
// review_depth carries; seam=none
|
|
test('carry --head from another checkout refuses; a receipt with gstack_version but no review_depth refuses', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const reviewed = commit(repo, { 'lib/util.js': lines(3), 'test/util.test.js': "test('a', () => {});\n" });
|
|
const body = path.join(repo.root, 'body.md');
|
|
expect(writeReceipt(repo, body).status).toBe(EXIT.ok);
|
|
const skipped = commit(repo, { 'test/util.test.js': "test.skip('a', () => {});\n" });
|
|
repo.git('checkout', '-q', reviewed);
|
|
const r = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main', '--head', skipped]);
|
|
expect(r.status).toBe(EXIT.refused);
|
|
expect(r.err).toContain(`--head ${skipped.slice(0, 12)} is not the checked-out HEAD ${reviewed.slice(0, 12)}`);
|
|
expect(receiptIn(body).head).toBe(reviewed);
|
|
repo.git('checkout', '-q', skipped);
|
|
rewrite(body, x => { delete x.review_depth; });
|
|
const noDepth = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
|
|
expect(noDepth.status).toBe(EXIT.refused);
|
|
expect(noDepth.err).toContain('records gstack_version but no review_depth');
|
|
}, 90_000);
|
|
});
|
|
|
|
describe('the review-loop exception (#3559)', () => {
|
|
// Value: protects=an accepted stop after a fix-check is a structured record the queue checks, not prose under REVIEW_LOOP;
|
|
// fails_when=write accepts it without a fix-check, with a foreign head or without the body line, or the queue ignores a
|
|
// body that dropped the line; seam=bindShippedReceipt
|
|
test('write refuses an exception with no fix-check, a foreign head or no body line, records it otherwise, and the queue checks the line', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const head = commit(repo, { 'lib/util.js': lines(3) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
const exception = { accepted_by: 'Garry', date: '2026-10-28', via: 'GSTA-147 relay', reason: 'fix-check edit is a comment; scope frozen', fix_check: { head, findings: 1 } };
|
|
const arg = (e: unknown) => ['--review-loop-exception', JSON.stringify(e)];
|
|
const loop = (n: number) => ['--review-loop', JSON.stringify({ passes: 2, blocking_found: 1, deferred: 0, fix_checks: n })];
|
|
fs.writeFileSync(body, '## What changed\n');
|
|
for (const [extra, why] of [
|
|
[[...arg(exception)], 'needs --review-loop with fix_checks >= 1'],
|
|
[[...arg(exception), ...loop(0)], 'needs --review-loop with fix_checks >= 1'],
|
|
[[...arg({ ...exception, fix_check: { head: 'a'.repeat(40), findings: 1 } }), ...loop(1)], 'is not ' + head.slice(0, 12) + ' or an ancestor of it'],
|
|
[[...arg({ ...exception, extra: 1 }), ...loop(1)], 'unknown key "extra"'],
|
|
[[...arg(exception), ...loop(1)], 'does not carry the exception line verbatim'],
|
|
] as Array<[string[], string]>) {
|
|
const out = writeReceipt(repo, body, extra);
|
|
expect(out.status).toBe(EXIT.fail);
|
|
expect(out.err).toContain('(REVIEW_LOOP_EXCEPTION_INVALID;');
|
|
expect(out.err).toContain(why);
|
|
}
|
|
expect(extractReceipt(fs.readFileSync(body, 'utf8')).found).toBe(false);
|
|
const line = reviewLoopExceptionLine(exception);
|
|
expect(line).toBe(`Review-loop exception: accepted by Garry 2026-10-28 via GSTA-147 relay: fix-check edit is a comment; scope frozen (fix-check at ${head.slice(0, 12)}, 1 finding)`);
|
|
fs.writeFileSync(body, `## What changed\n\n${line}\n`);
|
|
expect(writeReceipt(repo, body, [...arg(exception), ...loop(1)]).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_loop_exception).toEqual(exception);
|
|
expect(bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]).ok).toBe(true);
|
|
const dropped = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8').replace(line, ''), '7', [head]);
|
|
expect(!dropped.ok && dropped.code).toBe('QUEUE_RECEIPT_EXCEPTION_INVALID');
|
|
expect(!dropped.ok && dropped.message).toContain('does not carry the review-loop exception line verbatim');
|
|
const foreign = { ...exception, fix_check: { head: 'f'.repeat(40), findings: 1 } };
|
|
const forged = rewrite(body, x => { x.review_loop_exception = foreign; }).replace(line, reviewLoopExceptionLine(foreign));
|
|
const offBranch = bindShippedReceipt(repo.work, 'origin/main', forged, '7', [head]);
|
|
expect(!offBranch.ok && offBranch.code).toBe('QUEUE_RECEIPT_EXCEPTION_INVALID');
|
|
expect(!offBranch.ok && offBranch.message).toContain('is not the receipt head or an ancestor of it');
|
|
}, 90_000);
|
|
});
|
|
|
|
describe('the review-loop exception after the second full pass (TRUT-7, truther#1169)', () => {
|
|
// Value: protects=an owner who accepts an open finding under the two-round cap has a receipt path when /ship stopped after the
|
|
// second full pass (passes=2 blocking_found=1 deferred=1, no fix-check); fails_when=write refuses that stop, accepts one with
|
|
// fewer than two passes, no deferred finding, no finding id, both stop kinds or no body line, or the queue ignores the line;
|
|
// seam=gstack-ship-receipt write and bindShippedReceipt; no_claim=finish's routing
|
|
test('write records it with the deferred finding named by id; a stop that is neither kind is still refused', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const head = commit(repo, { 'lib/util.js': lines(3) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
const exception = { accepted_by: 'TRUT-1', date: '2026-11-03', via: 'TRUT-7 relay', reason: 'the open finding ships as a note under the two-round cap', second_pass: { head, deferred: ['specialist#3'] } };
|
|
const arg = (e: unknown) => ['--review-loop-exception', JSON.stringify(e)];
|
|
const loop = (passes: number, deferred: number) => ['--review-loop', JSON.stringify({ passes, blocking_found: 1, deferred, noted: 2, fixed: 2, scope: 'frozen' })];
|
|
fs.writeFileSync(body, '## What changed\n');
|
|
for (const [extra, why] of [
|
|
[[...arg(exception), ...loop(1, 1)], 'passes >= 2'],
|
|
[[...arg(exception), ...loop(2, 0)], 'deferred >= 1'],
|
|
[[...arg({ ...exception, second_pass: { head, deferred: [] } }), ...loop(2, 1)], 'second_pass.deferred must name'],
|
|
[[...arg({ ...exception, fix_check: { head, findings: 1 } }), ...loop(2, 1)], 'exactly one of fix_check or second_pass'],
|
|
[[...arg({ ...exception, second_pass: { head: 'a'.repeat(40), deferred: ['specialist#3'] } }), ...loop(2, 1)], 'is not ' + head.slice(0, 12) + ' or an ancestor of it'],
|
|
[[...arg(exception), ...loop(2, 1)], 'does not carry the exception line verbatim'],
|
|
] as Array<[string[], string]>) {
|
|
const out = writeReceipt(repo, body, extra);
|
|
expect([why, out.status, out.err.includes('(REVIEW_LOOP_EXCEPTION_INVALID;'), out.err.includes(why)]).toEqual([why, EXIT.fail, true, true]);
|
|
}
|
|
const line = reviewLoopExceptionLine(exception as never);
|
|
expect(line).toBe(`Review-loop exception: accepted by TRUT-1 2026-11-03 via TRUT-7 relay: the open finding ships as a note under the two-round cap (second full pass at ${head.slice(0, 12)}, deferred specialist#3)`);
|
|
fs.writeFileSync(body, `## What changed\n\n${line}\n`);
|
|
expect(writeReceipt(repo, body, [...arg(exception), ...loop(2, 1)]).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_loop_exception).toEqual(exception);
|
|
expect(bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]).ok).toBe(true);
|
|
const dropped = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8').replace(line, ''), '7', [head]);
|
|
expect(!dropped.ok && dropped.code).toBe('QUEUE_RECEIPT_EXCEPTION_INVALID');
|
|
}, 90_000);
|
|
});
|
|
|
|
describe('the queue and a scoped second pass (GSTA-152 D6)', () => {
|
|
// Value: protects=a receipt whose pass 2 was scoped (fix-check, Red Team, native adversarial; no specialists) READYs at the depth
|
|
// its head needs, with no schema change; fails_when=the queue reads review_loop.dispatches or passes as a depth signal; seam=bindShippedReceipt
|
|
test('a scoped pass 2 receipt at the required depth binds, its dispatch roles recorded as written', () => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const head = commit(repo, { 'lib/big.js': lines(80) });
|
|
const body = path.join(repo.root, 'body.md');
|
|
const loop = { passes: 2, blocking_found: 1, deferred: 0, fixed: 1, scope: 'frozen', dispatches: { specialist: 4, 'adversarial-native': 2, 'outside-voice': 1, 'fix-check': 1, 'red-team': 1 } };
|
|
expect(writeReceipt(repo, body, ['--review-loop', JSON.stringify(loop)]).status).toBe(EXIT.ok);
|
|
expect(receiptIn(body).review_loop).toMatchObject({ passes: 2, dispatches: loop.dispatches });
|
|
const bound = bindShippedReceipt(repo.work, 'origin/main', fs.readFileSync(body, 'utf8'), '7', [head]);
|
|
expect(bound.ok).toBe(true);
|
|
expect(bound.ok && bound.depthLine).toContain('REVIEW_DEPTH: receipt=light required=light');
|
|
expect(bound.ok && bound.depthLine).toMatch(/ rounds=2$/);
|
|
}, 60_000);
|
|
});
|
|
|
|
describe('GSTA-167 part 4a: the data rule never skips the receipt gate scan', () => {
|
|
// Value: protects=F1, receiptDepthAtHead decides the gate scan path-only, so a range whose only change is a data file is
|
|
// scanned and a failed scan records null, never an unscanned 0; fails_when=the scan is skipped through the data rule;
|
|
// seam=stub gstack-gate-diff that fails
|
|
test('a data-only range no code names is scanned (a failing scan records null); a markdown-only range is not', () => {
|
|
const run = (files: Record<string, string>) => {
|
|
const repo = makeFixtureRepo(); repos.push(repo);
|
|
const bin = path.join(repo.root, 'stub-bin');
|
|
fs.mkdirSync(bin);
|
|
fs.writeFileSync(path.join(bin, 'gstack-diff-scope'), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
|
fs.writeFileSync(path.join(bin, 'gstack-gate-diff'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
|
|
return receiptDepthAtHead(repo.work, 'main', bin, commit(repo, files));
|
|
};
|
|
const data = run({ 'wiki/post/data.json': '{"a": 1}\n' });
|
|
expect([data.class, data.runtime_lines, data.gate_scan.tagged]).toEqual(['docs', 0, null]);
|
|
const prose = run({ 'notes/a.md': '# a\n' });
|
|
expect([prose.class, prose.gate_scan.tagged]).toEqual(['docs', 0]);
|
|
}, 60_000);
|
|
});
|