fix(merge e85005482): targeted classes since the read accept an empty range; the wave's receipt tests pass main's targeted record

- lib/ship-receipt-depth.ts targetedClassesSince (main, GST-986) reads the range from the commit the targeted reader read to HEAD through headInputs, which since #3795 runs gstack-diff-scope's BASE_CONTAINS_HEAD check. When the reader read HEAD itself the range is empty, diff-scope reported base_contains_head, and every write with --review-targeted was refused (the product patch matched). It now passes merged=true (--merged), which skips only that check; an ancestor check already guards the range.
- test/review-exclude.test.ts and test/review-targeted.test.ts: since #3887 write refuses a money head with no targeted record and requires the record's head, so the wave's two writes that must succeed pass one, as main's own tests do (targetedAtHead in test/ship-receipt-depth.test.ts).
This commit is contained in:
garrytan committed 2026-11-03 00:08:39 +00:00
1 parent feeec2e760
commit d0c6089f82
3 files changed
+4 -3

No files matched your search

+1 -1
View File
@@ -295,7 +295,7 @@ export function bindShippedReceipt(root: string, baseRef: string, body: string,
*/
export function targetedClassesSince(root: string, from: string, head: string): readonly TargetedClass[] | null {
if (!git(root, ['merge-base', '--is-ancestor', from, head]).ok) return null;
const at = headInputs(root, from, head);
const at = headInputs(root, from, head, undefined, true);
let gateTagged: number | null = 0;
if (gateScanNeeded(at.c.paths, at.c.root)) {
const g = runScriptSync(path.join(BIN_DIR, 'gstack-gate-diff'), [from], { cwd: root, encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
+2 -1
View File
@@ -253,6 +253,7 @@ describe('the receipt binds the selection the reviewers read', () => {
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]);
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') })];
// Value: protects=a docs-only range whose only file sits under a base pattern and carries a gate tag binds the same selection at
// plan and receipt write, so the receipt can be written; fails_when=receiptReviewAtHead skips the gate scan when the paths are
// docs-class and excludes the file the plan kept as gate-tagged (/ship review of #3852, red-team#1: digests 652d0aa8 vs
@@ -291,7 +292,7 @@ describe('the receipt binds the selection the reviewers read', () => {
expect(missing.err).toContain(`field=review_exclude expected=${digest} recorded=none source=origin/main@`);
expect(missing.err).toContain('gstack-review-dispatch verify --ledger <ledger>');
expect(fs.existsSync(body)).toBe(false);
expect(writeReceipt(repo, body, ['--review-exclude', digest!]).status).toBe(EXIT.ok);
expect(writeReceipt(repo, body, ['--review-exclude', digest!, ...targetedAtHead(repo)]).status).toBe(EXIT.ok);
expect(receiptIn(body).review_exclude).toMatchObject({ digest, excluded: EXCLUDED.length, kept: KEPT.length });
onBase(repo, { 'docs/other.md': '# other\n' });
+1 -1
View File
@@ -376,7 +376,7 @@ describe('the /ship commands carry the targeted record to the receipt (GST-464)'
fs.writeFileSync(exclude, '*.log');
expect(spawnSync('bash', ['-c', entry!], { cwd: repo.work, encoding: 'utf8', timeout: 10_000 }).status).toBe(0);
expect(fs.readFileSync(exclude, 'utf8').split('\n')).toEqual(expect.arrayContaining(['*.log', '/.gstack/tmp/']));
fs.writeFileSync(path.join(repo.work, recordPath!), JSON.stringify({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), status: 'done', findings: 0, raised_by: 'targeted' }) + '\n');
fs.writeFileSync(path.join(repo.work, recordPath!), JSON.stringify({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), status: 'done', findings: 0, raised_by: 'targeted', head: repo.git('rev-parse', 'HEAD') }) + '\n');
const args = expand();
expect(args).toEqual(['--review-targeted', `@${recordPath}`]);
expect(repo.git('ls-files', '--others', '--exclude-standard')).toBe('');