mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
test: GST-986 failing-first cases for the targeted record, receipt write/carry and fix-check counters
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
82e9d7ec7a
commit
f0d9d42fda
2 files changed
+240
No files matched your search
@@ -247,3 +247,42 @@ describe('GST-951: the receipt counts every blocker the readers found', () => {
|
||||
expect(both.err).toContain('(REVIEW_SCOPE_INPUT;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GST-986 item 5: fix-check passes and their findings count from the ledgers', () => {
|
||||
// #3862 (reconstruction from its PR body; GST-963, pin 0ead1ce50): round 1's red-team CRITICAL sat in pass 1's row unclassified,
|
||||
// fixed in be9480829; the first granted fix-check found a CRITICAL, fixed in 0ead1ce50; the second fix-check was clean; the
|
||||
// targeted reader then raised 2 informational findings. Only pass 1 had a review-log row, so the receipt's tool counters read
|
||||
// blocking_found=0 fixed=0 fix_checks=0 and the lane hand-set blocking_found=2 fixed=2 fix_checks=2 (its third, the docs
|
||||
// re-check, ran outside the pass program). The outside voice's findings, which GStack Minder ruled below the floor, are left out.
|
||||
// Value: protects=a fix-check that ran through gstack-review-dispatch counts as a round, and a blocker a later fix-check
|
||||
// followed counts as fixed, without a review-log row; fails_when=summary counts fix_checks only from rows, or reports such
|
||||
// blockers as unrecorded; seam=gstack-review-scope summary --run; no_claim=noted (a floor ruling lives only in rows)
|
||||
test('#3862: two fix-check ledgers count as fix_checks=2, and the blockers they followed count as fixed', () => {
|
||||
const repo = branchWith([review(0, 1, [{ fingerprint: 'lib/tree-receipt.ts:120:logic', raised_by: 'red-team' }])]);
|
||||
const run = path.join(repo.root, 'run');
|
||||
const step = (dir: string, readers: Array<[string, string]>) => { pass(path.join(run, dir), readers); Bun.sleepSync(5); };
|
||||
step('p1', [['specialist-batched', CLEAN], ['red-team', '[CRITICAL] lib/tree-receipt.ts:120 the tree receipt floors the branch\'s own stamp row at the gated VERSION']]);
|
||||
step('fc1', [['fix-check', '[CRITICAL] lib/ship-queue-train-stamp.ts:88 the train stamp and verify checks pass no baseRef']]);
|
||||
step('fc2', [['fix-check', CLEAN]]);
|
||||
step('s11', [['targeted', '- [INFORMATIONAL] lib/stamp-only.ts:14 a comment names the old bound\n- [INFORMATIONAL] lib/stamp-only.ts:30 the side argument has no default test']]);
|
||||
const s = loop(summary(repo, '--run', run));
|
||||
expect(s).toMatchObject({ passes: 1, blocking_found: 2, fixed: 2, fix_checks: 2, rounds: 3, ledgers_read: 4 });
|
||||
expect(s.unrecorded).toBeUndefined();
|
||||
});
|
||||
|
||||
// Value: protects=a fix-check with both a row and a ledger counts once; a blocker no later fix-check followed stays unrecorded,
|
||||
// and one a row deferred stays deferred; fails_when=the ledger count adds to the row count, or every ledger blocker turns fixed;
|
||||
// seam=gstack-review-scope summary --run
|
||||
test('a fix-check counted by row and ledger counts once; the last pass\'s blocker stays unrecorded; a deferred blocker is not fixed', () => {
|
||||
const repo = branchWith([
|
||||
review(0, 1, [{ fingerprint: 'lib/a.ts:5:logic', severity: 'CRITICAL', blocking: true, action: 'deferred', raised_by: 'red-team' }]),
|
||||
fixCheck(30, []),
|
||||
]);
|
||||
const run = path.join(repo.root, 'run');
|
||||
const step = (dir: string, readers: Array<[string, string]>) => { pass(path.join(run, dir), readers); Bun.sleepSync(5); };
|
||||
step('p1', [['red-team', '[CRITICAL] lib/a.ts:5 a deferred defect outside the diff\n[CRITICAL] lib/b.ts:9 a fixed defect']]);
|
||||
step('fc1', [['fix-check', '[CRITICAL] lib/c.ts:3 the fix broke the caller']]);
|
||||
const s = loop(summary(repo, '--run', run));
|
||||
expect(s).toMatchObject({ passes: 1, blocking_found: 3, deferred: 1, fixed: 1, fix_checks: 1, rounds: 2, unrecorded: 1 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* GST-986 (GST-966 part 2): the targeted reader's record from verify to the receipt.
|
||||
* verify records what the reader did and the commit it read; write binds that record only to a receipt whose product patch
|
||||
* is the one the reader read; write and carry refuse a receipt the queue's targeted verdict would refuse; /ship's receipt
|
||||
* writes pass the record. Cases come from gstack-evals#140, C5-36/C6-36 item 5 and GSTA-234's outside-voice fixture on #3859.
|
||||
*/
|
||||
import { afterEach, describe, expect, setDefaultTimeout, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT } from '../lib/headless-artifacts';
|
||||
import { checkoutTree } from '../lib/review-brief';
|
||||
import { TARGETED_RULES, targetedBriefSha } from '../lib/review-targeted';
|
||||
import { extractReceipt, placeReceiptFirst } from '../lib/ship-receipt';
|
||||
import { receiptDepthAtHead } from '../lib/ship-receipt-depth';
|
||||
import { cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
setDefaultTimeout(180_000);
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const repos: FixtureRepo[] = [];
|
||||
afterEach(() => { while (repos.length) cleanup(repos.pop()!); });
|
||||
|
||||
function moneyBranch(): { repo: FixtureRepo; base: string; head: string } {
|
||||
const repo = makeFixtureRepo({ prefix: 'gst986-' });
|
||||
repos.push(repo);
|
||||
repo.git('checkout', '-q', '-b', 'feat');
|
||||
fs.mkdirSync(path.join(repo.work, 'src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 5;\n');
|
||||
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'charge');
|
||||
return { repo, base: repo.git('merge-base', 'origin/main', 'HEAD'), head: repo.git('rev-parse', 'HEAD') };
|
||||
}
|
||||
const commit = (repo: FixtureRepo, file: string, text: string, msg: string) => {
|
||||
fs.mkdirSync(path.dirname(path.join(repo.work, file)), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, file), text);
|
||||
repo.git('add', '-A'); repo.git('commit', '-q', '-m', msg);
|
||||
return repo.git('rev-parse', 'HEAD');
|
||||
};
|
||||
|
||||
/** One targeted read through the real dispatch steps; `reply` is the reader's raw output after its REVIEWED_TREE line. */
|
||||
function targetedRead(repo: FixtureRepo, base: string, dir: string, reply: string | null, sent = true): string {
|
||||
const out = path.join(repo.root, dir);
|
||||
const ledger = path.join(out, 'review-ledger.jsonl');
|
||||
const render = runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', 'money', '--diff-base', base, '--out', out]);
|
||||
expect(render.status, render.err).toBe(0);
|
||||
const file = /file=(\S+)/.exec(render.out)![1]!;
|
||||
if (sent) expect(runBin(repo, 'gstack-review-dispatch', ['sent', '--ledger', ledger, '--role', 'targeted', '--sent', file]).status).toBe(0);
|
||||
if (reply === null) return ledger;
|
||||
const output = path.join(out, 'targeted.out');
|
||||
fs.writeFileSync(output, `REVIEWED_TREE: ${checkoutTree(repo.work)}\n${reply}\n`);
|
||||
const merged = path.join(out, 'merged.jsonl');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['collect', '--ledger', ledger, '--role', 'targeted', '--output', output, '--merged', merged]).status).toBe(0);
|
||||
const judged = path.join(out, 'judged.jsonl');
|
||||
fs.writeFileSync(judged, fs.readFileSync(merged, 'utf8').split('\n').filter(Boolean).map(l => JSON.stringify({ id: JSON.parse(l).id, severity: JSON.parse(l).label }) + '\n').join(''));
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['judge', '--ledger', ledger, '--merged', merged, '--judged', judged]).status).toBe(0);
|
||||
return ledger;
|
||||
}
|
||||
const verify = (repo: FixtureRepo, ledger: string, record?: string) =>
|
||||
runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger, ...(record ? ['--targeted-out', record] : [])]);
|
||||
const recordLine = (out: string) => out.split('\n').find(l => l.startsWith('TARGETED_RECORD:')) ?? '';
|
||||
const finding = (line: number, summary: string) => JSON.stringify({ severity: 'INFORMATIONAL', confidence: 7, path: 'src/charge.ts', line, category: 'money', summary, fingerprint: `src/charge.ts:${line}:money`, specialist: 'targeted' });
|
||||
const write = (repo: FixtureRepo, args: string[]) => runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', ...args, '--json']);
|
||||
|
||||
describe('verify records the targeted reader\'s outcome and the commit it read (item 1)', () => {
|
||||
// Value: protects=a targeted reader that completed reads as done, with its findings counted, however the head moves after it
|
||||
// (gstack-evals#140, C5-36 and C6-36 p2: Step 11 fixes moved the head and the clean or 2-finding read printed status=failed
|
||||
// findings=0); a reader that did not complete stays failed; fails_when=verify fails a completed read because HEAD moved, zeroes
|
||||
// its findings, or marks a read with no matching dispatch or output done; seam=gstack-review-dispatch verify --targeted-out;
|
||||
// no_claim=binding the record to the receipt (write, below)
|
||||
test('a clean read and a 2-finding read stay done after a later fix commit; an unsent or uncollected read is failed', () => {
|
||||
const { repo, base, head } = moneyBranch();
|
||||
const clean = targetedRead(repo, base, 'clean', 'NO FINDINGS');
|
||||
const found = targetedRead(repo, base, 'found', `${finding(1, 'the cap is never enforced')}\n${finding(1, 'the cap has no unit test')}`);
|
||||
const unsent = targetedRead(repo, base, 'unsent', 'NO FINDINGS', false);
|
||||
const silent = targetedRead(repo, base, 'silent', null);
|
||||
commit(repo, 'src/charge.ts', 'export const spend_cap_usd = 5; // enforced in run()\n', 'Step 11 fix after the reader');
|
||||
|
||||
const record = path.join(repo.root, 'targeted.json');
|
||||
const v = verify(repo, clean, record);
|
||||
expect(recordLine(v.out)).toStartWith(`TARGETED_RECORD: classes=money status=done findings=0 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES}`);
|
||||
expect(JSON.parse(fs.readFileSync(record, 'utf8'))).toEqual({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), status: 'done', findings: 0, raised_by: 'targeted', head });
|
||||
expect(recordLine(verify(repo, found).out)).toStartWith('TARGETED_RECORD: classes=money status=done findings=2 ');
|
||||
expect(recordLine(verify(repo, unsent).out)).toStartWith('TARGETED_RECORD: classes=money status=failed ');
|
||||
expect(recordLine(verify(repo, silent).out)).toStartWith('TARGETED_RECORD: classes=money status=failed findings=0 ');
|
||||
});
|
||||
|
||||
// Value: protects=a read rendered on uncommitted tracked edits never reads as done (nothing proves which patch it read);
|
||||
// fails_when=the item 1 repair accepts it; seam=gstack-review-dispatch render + verify
|
||||
test('a read rendered on uncommitted tracked edits is failed', () => {
|
||||
const { repo, base } = moneyBranch();
|
||||
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 6;\n');
|
||||
const dirty = targetedRead(repo, base, 'dirty', 'NO FINDINGS');
|
||||
repo.git('checkout', '--', 'src/charge.ts');
|
||||
expect(recordLine(verify(repo, dirty).out)).toStartWith('TARGETED_RECORD: classes=money status=failed ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('write binds a targeted record only to the product patch the reader read (item 2)', () => {
|
||||
// Value: protects=a record verified at H1 never vouches for H2's new product code (GSTA-234's outside-voice CRITICAL on #3859:
|
||||
// write rebound the H1 record to H2's product patch-id and ready printed verdict=ok); a later commit that leaves the product
|
||||
// patch alone (a test, the version stamp) still binds; fails_when=write accepts the stale record, rejects the unchanged
|
||||
// product, or accepts a record that names no commit; seam=gstack-ship-receipt write --review-targeted
|
||||
test('GSTA-234\'s fixture: an auth commit after verify is refused; a test-only commit binds', () => {
|
||||
const { repo, base, head } = moneyBranch();
|
||||
const record = path.join(repo.root, 'targeted.json');
|
||||
expect(verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record).status).toBe(0);
|
||||
|
||||
commit(repo, 'test/charge.test.ts', 'test("cap", () => {});\n', 'test only');
|
||||
const testOnly = write(repo, ['--review-targeted', `@${record}`]);
|
||||
expect(testOnly.status, testOnly.err).toBe(EXIT.ok);
|
||||
expect(JSON.parse(testOnly.out).review_targeted).toMatchObject({ classes: ['money'], status: 'done' });
|
||||
|
||||
commit(repo, 'src/session.ts', 'export function checkSession(token: string) { return token.length > 0; }\n', 'auth change after verify');
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
const stale = write(repo, ['--review-targeted', `@${record}`, '--body', body]);
|
||||
expect(stale.status).toBe(EXIT.fail);
|
||||
expect(stale.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
|
||||
expect(stale.err).toContain('(RECEIPT_INVALID');
|
||||
expect(fs.existsSync(body)).toBe(false);
|
||||
|
||||
const { head: _drop, ...headless } = JSON.parse(fs.readFileSync(record, 'utf8'));
|
||||
const old = write(repo, ['--review-targeted', JSON.stringify(headless)]);
|
||||
expect(old.status).toBe(EXIT.fail);
|
||||
expect(old.err).toContain('review_targeted.head');
|
||||
});
|
||||
});
|
||||
|
||||
describe('write and carry refuse a receipt the queue\'s targeted verdict refuses (item 4, #3854)', () => {
|
||||
// Value: protects=/ship never publishes a receipt whose head needs a targeted class it did not cover (gstack-evals#140 wrote
|
||||
// gstack_version with receipt=legacy); a head that needs none writes as before; fails_when=write records no or a failed pass
|
||||
// on a money head, or refuses a head without classes; seam=gstack-ship-receipt write + receiptTargetedVerdict
|
||||
test('write: missing and failed records are refused on a money head; a covered or classless head writes', () => {
|
||||
const { repo, base } = moneyBranch();
|
||||
const missing = write(repo, []);
|
||||
expect(missing.status).toBe(EXIT.fail);
|
||||
expect(missing.err).toContain('REVIEW_TARGETED: required=money receipt=legacy verdict=missing');
|
||||
expect(missing.err).toMatch(/requires the targeted reader on money, but the receipt records no targeted pass.*\(RECEIPT_INVALID/);
|
||||
const record = path.join(repo.root, 'targeted.json');
|
||||
verify(repo, targetedRead(repo, base, 'h1', 'NO FINDINGS'), record);
|
||||
const failed = write(repo, ['--review-targeted', JSON.stringify({ ...JSON.parse(fs.readFileSync(record, 'utf8')), status: 'failed' })]);
|
||||
expect(failed.status).toBe(EXIT.fail);
|
||||
expect(failed.err).toContain('targeted pass failed');
|
||||
expect(write(repo, ['--review-targeted', `@${record}`]).status).toBe(EXIT.ok);
|
||||
|
||||
const plain = makeFixtureRepo({ prefix: 'gst986-plain-' });
|
||||
repos.push(plain);
|
||||
plain.git('checkout', '-q', '-b', 'feat');
|
||||
commit(plain, 'docs/notes.md', '# notes\n', 'docs');
|
||||
const none = write(plain, []);
|
||||
expect(none.status, none.err).toBe(EXIT.ok);
|
||||
expect(JSON.parse(none.out)).not.toHaveProperty('review_targeted');
|
||||
});
|
||||
|
||||
// Value: protects=carry never rebinds a receipt the queue would refuse on targeted coverage (#3854: carry checked depth only);
|
||||
// a legacy receipt and a covered one carry as before; fails_when=carry prints CARRY: carried for an uncovered money head, or
|
||||
// refuses a covered or legacy one; seam=gstack-ship-receipt carry
|
||||
test('carry: an uncovered versioned receipt is refused; a covered or legacy one carries', () => {
|
||||
const { repo, head } = moneyBranch();
|
||||
const depth = receiptDepthAtHead(repo.work, 'main', path.join(ROOT, 'bin'), head);
|
||||
const versioned = { schema_version: 1, pr: 5, head, base: 'main', gstack_version: '1.92.75.0', review_depth: depth };
|
||||
const covered = { ...versioned, review_targeted: { classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), product_patch_id: 'p', status: 'done', findings: 0, raised_by: 'targeted' } };
|
||||
commit(repo, 'test/charge.test.ts', 'test("cap", () => {});\n', 'test only');
|
||||
let n = 0;
|
||||
const carry = (receipt: object) => {
|
||||
const body = path.join(repo.root, `body-${++n}.md`);
|
||||
fs.writeFileSync(body, placeReceiptFirst('## Summary\n\nProse.\n', receipt as any));
|
||||
return { ...runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body], { GSTACK_STATE_ROOT: path.join(repo.root, 'state') }), body };
|
||||
};
|
||||
const uncovered = carry(versioned);
|
||||
expect(uncovered.status).toBe(EXIT.fail);
|
||||
expect(uncovered.err).toMatch(/requires the targeted reader on money, but the receipt records no targeted pass.*\(RECEIPT_CARRY_REFUSED/);
|
||||
expect(extractReceipt(fs.readFileSync(uncovered.body, 'utf8')).receipt!.head).toBe(head);
|
||||
const ok = carry(covered);
|
||||
expect(ok.status, ok.err).toBe(EXIT.ok);
|
||||
expect(ok.out).toContain('CARRY: carried from');
|
||||
const legacy = carry({ schema_version: 1, pr: 5, head, base: 'main' });
|
||||
expect(legacy.status, legacy.err).toBe(EXIT.ok);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/ship\'s receipt writes pass the targeted record (item 3, #3853)', () => {
|
||||
// Value: protects=the record Step 11's verify writes reaches every receipt write /ship runs (Step 19, the handoff, the driver);
|
||||
// fails_when=a write command omits --review-targeted or reads another path than verify writes; seam=the generated sections'
|
||||
// command text expanded by bash; no_claim=the agent following it (prose)
|
||||
test('verify writes .gstack/tmp/targeted.json and the Step 19, handoff and driver writes read it', () => {
|
||||
const section = (name: string) => fs.readFileSync(path.join(ROOT, 'ship', 'sections', `${name}.md`), 'utf8');
|
||||
const recordPath = /verify --targeted-out (\S+?)`/.exec(section('adversarial'))?.[1];
|
||||
expect(recordPath).toBe('.gstack/tmp/targeted.json');
|
||||
const writeLine = section('pr-body').split('\n').find(l => l.includes('gstack-ship-receipt write --pr'))!;
|
||||
const optional = /\$\(\[ -s (\S+) \] && echo (--review-targeted @\S+)\)/.exec(writeLine);
|
||||
expect(optional?.slice(1)).toEqual([recordPath!, `--review-targeted @${recordPath}`]);
|
||||
expect(section('handoff')).toContain(`When Step 11 saved \`${recordPath}\`, add \`--review-targeted @${recordPath}\` to that write.`);
|
||||
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain(`[--review-targeted @${recordPath}]`);
|
||||
|
||||
const { repo } = moneyBranch();
|
||||
const expand = () => spawnSync('bash', ['-c', `printf '%s\\n' ${optional![0]}`], { cwd: repo.work, encoding: 'utf8', timeout: 10_000 }).stdout.split('\n').filter(Boolean);
|
||||
expect(expand()).toEqual([]);
|
||||
fs.mkdirSync(path.join(repo.work, '.gstack', 'tmp'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, recordPath!), '{}\n');
|
||||
expect(expand()).toEqual(['--review-targeted', `@${recordPath}`]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user