Files
msd-core/tests/antigravity-repo-grounding.test.cjs
Tom Boucher 40ce95f882 fix(#2358): scope review.md and ship.md temp files to a per-run mktemp directory (#2433)
* fix(#2358): scope review workflow temp files to a per-run mktemp dir

/gsd-review wrote every prompt/section/output temp file to a hardcoded
/tmp path keyed only on the phase number, so two GSD projects sharing
a small phase number collide on the exact same path and a crashed
run's leftover file becomes bait a later, unrelated run can silently
read. ship.md's external peer-review stderr capture was strictly
worse — one shared, unqualified path across every project/phase/run.

Thread a single mktemp -d "${TMPDIR:-/tmp}/gsd-review.XXXXXX" run
directory through every review.md temp path (67 sites) via a new
{run_dir}/$RUN_DIR placeholder, mirroring the existing {phase}
substitution mechanism, and clean it up at the end of the run. Route
ship.md's stderr capture through a per-run mktemp file the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): regenerate fixtures + lint gate-prep

* fix(#2358): repair failing tests after gate verification

* fix(#2358): thread RUN_DIR scoping into reviewer-instances.md (#1517)

review.md's own invoke_reviewers step lazily loads
gsd-core/references/reviewer-instances.md for the review.reviewer_instances
codepath, but that doc was missed when review.md and ship.md were moved to
the run-scoped {run_dir} temp directory. It still read the combined prompt
from the old /tmp/gsd-review-prompt-{phase}.md (which build_prompt no longer
writes, breaking reviewer-instances functionality outright) and wrote each
instance's output to the old unscoped /tmp/gsd-review-${INSTANCE_NAME}-{phase}.md,
leaving the exact cross-project temp-file collision bug open for that code
path. Both paths now thread through {run_dir}, matching every other reviewer
block in review.md.

Extends the existing #2358 regression test with assertions pinning
reviewer-instances.md's prompt read and output write to {run_dir}, and adds
the Fixed changeset fragment. Regenerated the golden-install-parity content
hashes for reviewer-instances.md via `npm run gen:golden` (paths unchanged;
only the modified file's hash moved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#2358): backfill changeset pr (#2433)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:12:03 -04:00

193 lines
8.8 KiB
JavaScript

// allow-test-rule: source-text-is-the-product (see #2073)
// gsd-core/workflows/review.md is a workflow document whose bash blocks ARE
// what /gsd-review loads and executes at runtime. Asserting the invocation
// shape asserts the deployed contract — this is behavioral coverage of the
// workflow, not a source-grep over application code.
/**
* Antigravity reviewer repo-grounding tests (#2176)
*
* The agy block invoked the CLI without granting it the repo under review:
* no --add-dir on either invocation arm, and no absolute repo-root anchor in
* _AGY_PROMPT. The agent frequently anchored on its own
* ~/.gemini/antigravity-cli/scratch dir, reviewed the plan text in isolation
* (the exact failure the block's Review Instructions forbid), and its
* ungrounded verdict flowed into the Consensus Summary at full weight,
* undetected.
*
* These tests pin the fix: capability-probed --add-dir (mirrors the Codex
* bypass-flag probe), absolute-root prompt anchor (agy AND the cursor-agent
* block, which shared the anchor gap), a mandated self-report line, a stamped
* blind-review marker, and consensus down-weighting of marked reviews.
* Each assertion fails against the pre-fix block.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const REVIEW_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'review.md');
function reviewContent() {
return fs.readFileSync(REVIEW_PATH, 'utf-8');
}
function agyBashBlock() {
const fences = reviewContent().match(/```bash[\s\S]*?```/g) || [];
const agy = fences.find((f) => /\bagy\b/.test(f) && /gsd-review-antigravity/.test(f));
assert.ok(agy, 'review.md should contain the agy invocation bash block');
return agy;
}
function cursorBashBlock() {
const fences = reviewContent().match(/```bash[\s\S]*?```/g) || [];
const cursor = fences.find((f) => /cursor-agent -p/.test(f));
assert.ok(cursor, 'review.md should contain the cursor-agent invocation bash block');
return cursor;
}
describe('Antigravity reviewer repo grounding in /gsd-review (#2176)', () => {
test('probes agy for --add-dir support (capability-probe idiom, mirrors the Codex block)', () => {
const block = agyBashBlock();
assert.ok(
/agy --help 2>\/dev\/null \| grep -q -- '--add-dir'/.test(block),
'agy block must capability-probe --add-dir via `agy --help | grep -q` so older CLIs still run',
);
});
test('passes --add-dir with the repo root when supported', () => {
const block = agyBashBlock();
assert.ok(
/set -- "\$@" --add-dir "\$_AGY_WS"/.test(block),
'the probed arm must append --add-dir "$_AGY_WS" so both invocation arms (which expand "$@") receive it',
);
});
test('_AGY_PROMPT is anchored to the absolute repo root', () => {
const block = agyBashBlock();
const promptLine = block.split('\n').find((l) => l.startsWith('_AGY_PROMPT='));
assert.ok(promptLine, 'agy block must define _AGY_PROMPT');
assert.ok(
/\$_AGY_WS/.test(promptLine),
'_AGY_PROMPT must embed the absolute repo root ($_AGY_WS) so repo-relative references resolve on the no---add-dir fallback',
);
});
test('_AGY_PROMPT mandates a REVIEWED-WITHOUT-REPO-ACCESS self-report', () => {
const block = agyBashBlock();
const promptLine = block.split('\n').find((l) => l.startsWith('_AGY_PROMPT='));
assert.ok(
/REVIEWED-WITHOUT-REPO-ACCESS/.test(promptLine),
'_AGY_PROMPT must require the exact self-report line when the reviewer cannot read the repo',
);
});
test('stamps a blind-review marker on self-reported or scratch-anchored output', () => {
const block = agyBashBlock();
assert.ok(
/head -5 [^|]*\| grep -q 'REVIEWED-WITHOUT-REPO-ACCESS'/.test(block),
'the self-report tell must be anchored to the head of the output, not a whole-body substring',
);
assert.ok(
/grep -qiE '\(workspace\|working\) \(directory\|dir\)\.\{0,40\}antigravity-cli\/scratch'/.test(block),
'the scratch tell must be anchored to a workspace-declaration phrasing whose bridge (.{0,40}) can span the dotted ~/.gemini/ path prefix',
);
assert.ok(
/\[reviewed-without-repo-access\]/.test(block),
'detected blind reviews must be stamped with the [reviewed-without-repo-access] marker',
);
});
test('marker stamping avoids sed -i (BSD/GNU divergence) — uses temp file + mv', () => {
const block = agyBashBlock();
assert.ok(!/sed -i/.test(block), 'agy block must not use sed -i (BSD vs GNU incompatibility)');
assert.ok(
/\.tmp && \\?\s*\n?\s*mv /.test(block),
'marker stamping should rewrite via temp file + mv',
);
});
test('Consensus Summary down-weights marked blind reviews', () => {
const content = reviewContent();
const consensusIdx = content.indexOf('## Consensus Summary');
assert.ok(consensusIdx >= 0, 'review.md should contain the Consensus Summary section');
const consensus = content.slice(consensusIdx, consensusIdx + 2000);
assert.ok(
/\[reviewed-without-repo-access\]/.test(consensus),
'consensus instructions must reference the blind-review marker',
);
assert.ok(
/REVIEWED-WITHOUT-REPO-ACCESS/.test(consensus),
'consensus instructions must also honor the raw self-report line',
);
assert.ok(
/not count its verdict at full consensus weight/.test(consensus),
'marked reviews must be down-weighted, not counted at full weight',
);
});
test('blind-review detection behaves correctly on synthetic transcripts', () => {
// Behavioral, not string-on-string: extract the actual detection compound
// from the fence, point it at a temp file, and run it through bash for
// ungrounded and grounded transcript shapes.
const os = require('os');
const { execFileSync } = require('node:child_process');
const { toPosixPath } = require('../gsd-core/bin/lib/shell-command-projection.cjs');
const block = agyBashBlock();
const m = block.match(/\{ head -5[\s\S]*?\}; then/);
assert.ok(m, 'detection compound not found in the agy block');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agy-detect-'));
const out = path.join(tmp, 'review-out.md');
const detect = m[0]
.replace(/\}; then$/, '}')
// Convert the native path to POSIX form so it survives bash on Windows
// runners (Git Bash accepts D:/... but eats backslashes).
.replaceAll('{run_dir}/gsd-review-antigravity.md', toPosixPath(out));
const runDetect = (content) => {
fs.writeFileSync(out, content);
try {
execFileSync('bash', ['-c', detect], { stdio: 'ignore' });
return true; // exit 0 → blind review detected
} catch {
return false;
}
};
try {
// Ungrounded tells — must be stamped
assert.equal(runDetect('REVIEWED-WITHOUT-REPO-ACCESS\n\n## Review\nPlan-only review.\n'), true,
'self-report line in the head must be detected');
assert.equal(runDetect('## Review\nMy working directory is ~/.gemini/antigravity-cli/scratch.\nPlan-only review.\n'), true,
'full dotted scratch path in a workspace declaration must be detected (#2184 re-review Major)');
assert.equal(runDetect('Workspace directory: /home/me/.gemini/antigravity-cli/scratch\n'), true,
'colon-style workspace declaration must be detected');
// Grounded shapes — must NOT be stamped
assert.equal(runDetect('## Review\nVerified hooks/gsd-statusline.js against the plan. Solid.\n'), false,
'ordinary grounded review must not be stamped');
assert.equal(runDetect('## Review\nThe workflow mentions antigravity-cli/scratch as the agy scratch dir; the guard there is correct.\n'), false,
'grounded review merely quoting the scratch path must not be stamped');
assert.equal(
runDetect('## Review\n\nGrounded findings below.\n\n### Details\nLine 20 of the workflow mentions REVIEWED-WITHOUT-REPO-ACCESS as the self-report marker; fine.\n'),
false,
'self-report string quoted beyond the first 5 lines must not be stamped');
} finally {
require('./helpers.cjs').cleanup(tmp);
}
});
test('cursor-agent prompt carries the same absolute-root anchor (identical gap, #2176 AC5)', () => {
const block = cursorBashBlock();
assert.ok(
/_CURSOR_ROOT="\$\(git rev-parse --show-toplevel 2>\/dev\/null \|\| pwd\)"/.test(block),
'cursor anchor must resolve the repo TOP-LEVEL (rev-parse, not bare pwd) so subdirectory invocations anchor correctly',
);
const promptLine = block.split('\n').find((l) => l.startsWith('CURSOR_PROMPT_ARG='));
assert.ok(promptLine, 'cursor block must define CURSOR_PROMPT_ARG');
assert.ok(
/repository under review is at \$_CURSOR_ROOT/.test(promptLine),
'CURSOR_PROMPT_ARG must anchor repo-relative references to the absolute repo root',
);
});
});