* 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>
110 lines
5.0 KiB
JavaScript
110 lines
5.0 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 Antigravity
|
|
// invocation shape asserts the deployed contract — this is behavioral coverage
|
|
// of the workflow, not a source-grep over application code.
|
|
|
|
/**
|
|
* Antigravity (agy) reviewer invocation tests (#2073)
|
|
*
|
|
* The agy block in /gsd-review had three real-world failure modes on agy 1.0.16:
|
|
* 1. inline `-p "$(cat <prompt>)"` overflowed the exec arg list on a large prompt
|
|
* 2. a pinned model that 404'd exited 0 with empty stdout AND an empty transcript
|
|
* (no --model escape hatch; the generic Step 3 stub gave no diagnostic)
|
|
* 3. a pre-session stall hung past --print-timeout (which can't fire before a
|
|
* session exists) because there was no external wall-clock `timeout`
|
|
* Plus a stale maintainer note claiming agy has no --model flag.
|
|
*
|
|
* These tests pin the corrected invocation shape in gsd-core/workflows/review.md.
|
|
*/
|
|
|
|
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 agyBashBlock() {
|
|
const content = fs.readFileSync(REVIEW_PATH, 'utf-8');
|
|
const fences = content.match(/```bash[\s\S]*?```/g) || [];
|
|
// Target the INVOCATION block (the fence that writes the antigravity review
|
|
// output), not the `command -v agy` detection one-liner.
|
|
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;
|
|
}
|
|
|
|
describe('Antigravity (agy) reviewer invocation in /gsd-review (#2073)', () => {
|
|
test('review.md exists', () => {
|
|
assert.ok(fs.existsSync(REVIEW_PATH), 'review.md should exist');
|
|
});
|
|
|
|
test('#2073 mode 1 — does NOT inline the prompt via "$(cat ...)" (arg-list overflow)', () => {
|
|
const block = agyBashBlock();
|
|
assert.ok(
|
|
!/"\$\(cat/.test(block),
|
|
'agy must not inline the prompt via "$(cat …)" — a large review prompt overflows the exec arg list',
|
|
);
|
|
});
|
|
|
|
test('#2073 mode 1 — uses a file-reference prompt (mirrors the Cursor block)', () => {
|
|
const block = agyBashBlock();
|
|
assert.ok(
|
|
/Read the file at \{run_dir\}\/gsd-review-prompt\.md/.test(block),
|
|
'agy should reference the prompt by file path, not inline it',
|
|
);
|
|
});
|
|
|
|
test('#2073 mode 3 — pairs agy with an external wall-clock killer when available (timeout/gtimeout probe)', () => {
|
|
const block = agyBashBlock();
|
|
// Capability probe for GNU `timeout` and macOS `gtimeout` (stock macOS has neither).
|
|
assert.match(block, /command -v timeout/, 'agy block should probe for the `timeout` killer');
|
|
assert.match(block, /command -v gtimeout/, 'agy block should probe for `gtimeout` (macOS Homebrew)');
|
|
// The external cap (600s) is >= agy's native --print-timeout (540s) so it only
|
|
// backstops a pre-session stall, never cuts a healthy run.
|
|
assert.match(block, /600 agy --print-timeout 540s/, 'external cap (600s) must be >= --print-timeout (540s)');
|
|
// Graceful fallback when no external killer is available (stock macOS).
|
|
assert.match(block, /else\n\s*agy --print-timeout 540s/,
|
|
'agy block must fall back to --print-timeout alone when no external killer is available (macOS)');
|
|
});
|
|
|
|
test('#2073 mode 3 — stdin is tied to /dev/null (no tty stall)', () => {
|
|
const block = agyBashBlock();
|
|
assert.ok(
|
|
/<\/dev\/null/.test(block),
|
|
'agy invocation should redirect stdin from /dev/null so it never blocks on a tty',
|
|
);
|
|
});
|
|
|
|
test('#2073 mode 2 — wires review.models.agy via --model when configured', () => {
|
|
const block = agyBashBlock();
|
|
assert.match(block, /--model "\$AGY_MODEL"/, 'agy block should pass --model "$AGY_MODEL" when set');
|
|
});
|
|
|
|
test('#2073 mode 2 — Step 3 stub surfaces a diagnostic from agy cli.log (not just a generic stub)', () => {
|
|
const block = agyBashBlock();
|
|
assert.ok(
|
|
/cli\.log/.test(block),
|
|
'the empty-output stub should inspect agy cli.log for a model-availability diagnostic (NOT_FOUND / agent executor error)',
|
|
);
|
|
});
|
|
|
|
test('#2073 — stale "no --model flag" maintainer note is corrected', () => {
|
|
const content = fs.readFileSync(REVIEW_PATH, 'utf-8');
|
|
assert.ok(
|
|
!/No .{0,4}-m.{0,4}\/.{0,4}--model.{0,4} flag/i.test(content),
|
|
'the stale maintainer note claiming agy has no --model flag must be corrected (--model exists since ~1.0.3)',
|
|
);
|
|
});
|
|
|
|
test('#2073 — review.models.agy is documented as supported (not "reserved for future")', () => {
|
|
const content = fs.readFileSync(REVIEW_PATH, 'utf-8');
|
|
assert.ok(
|
|
!/review\.models\.agy is reserved for future/i.test(content),
|
|
'review.models.agy is now wired (passed as --model); the "reserved for future" comment must be updated',
|
|
);
|
|
});
|
|
});
|