Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 194 additions & 12 deletions packages/cli/src/commands/review/agent-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,61 @@ describe('agent-prompt (command boundary)', () => {
rmSync(dir, { recursive: true, force: true });
}
});

it('lets --role reverse-audit --chunk N through and keys the record by its chunk', () => {
// The unit tests build the launch prompt directly, bypassing the guard and the
// key derivation. This drives the real handler: the guard must let the one legal
// role+chunk combo through, the record key must carry the chunk — the delivery
// check finds the recorded prompt by that key — and the brief it points at must
// read that chunk alone, so brief and launch prompt agree on one chunk's range.
const dir = mkdtempSync(join(tmpdir(), 'ap-ra-'));
try {
const plan = join(dir, 'plan.json');
writeFileSync(plan, JSON.stringify(PLAN));
expect(() =>
(agentPromptCommand.handler as (a: unknown) => void)({
plan,
role: 'reverse-audit',
chunk: 14,
}),
).not.toThrow();
const recorded = readRecordedPrompts(plan);
expect([...recorded.keys()]).toEqual(['reverse-audit--chunk-14']);
const briefText = readFileSync(
briefPath(plan, 'reverse-audit--chunk-14'),
'utf8',
);
expect(briefText).toContain('offset=4024, limit=176'); // chunk 14 only
expect(briefText).not.toContain('offset=3807'); // not chunk 13
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it('drives the verify role end-to-end through the handler', () => {
// verify is covered via buildRoleBrief / buildRoleLaunchPrompt directly; this is
// the one new role whose full handler path — brief write, record key, and the
// `output: 'verdicts'` branch of tail() — was not driven end-to-end.
const dir = mkdtempSync(join(tmpdir(), 'ap-verify-'));
try {
const plan = join(dir, 'plan.json');
writeFileSync(plan, JSON.stringify(PLAN));
expect(() =>
(agentPromptCommand.handler as (a: unknown) => void)({
plan,
role: 'verify',
}),
).not.toThrow();
const recorded = readRecordedPrompts(plan);
expect([...recorded.keys()]).toEqual(['verify']);
const briefText = readFileSync(briefPath(plan, 'verify'), 'utf8');
// The verdict branch: Exclusion Criteria yes, finding format no.
expect(briefText).toContain('What is NOT a finding');
expect(briefText).not.toContain('**Anchor:**');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

// The half of the fan-out this command did not cover. Measured against one real
Expand Down Expand Up @@ -388,24 +443,89 @@ describe('buildWholeDiffBlock — the agents that walk the whole diff', () => {
});

it.each([
['none of the three', {}],
['chunk + whole-diff', { chunk: 13, 'whole-diff': true }],
['chunk + role', { chunk: 13, role: '2' }],
['whole-diff + role', { 'whole-diff': true, role: '2' }],
['all three', { chunk: 13, 'whole-diff': true, role: '2' }],
])('rejects a call that names %s', (_, extra) => {
// Three mutually exclusive modes: a territory chunk, a named role, or the
// bare whole-diff block. A run that named none used to fall through to the
// chunk builder with `undefined` and blame the plan for "no chunk undefined";
['none of the three', {}, /exactly one of/],
[
'chunk + whole-diff',
{ chunk: 13, 'whole-diff': true },
/--whole-diff builds the diff-reading block alone/,
],
[
'a non-reverse role + chunk',
{ chunk: 13, role: '2' },
// The message names the set it read from `acceptsChunk`, not a hardcoded role.
/only for a per-chunk role \(reverse-audit\); role "2" does not take --chunk/,
],
[
'whole-diff + role',
{ 'whole-diff': true, role: '2' },
/--whole-diff builds the diff-reading block alone/,
],
[
'whole-diff + file',
{ 'whole-diff': true, file: 'foo.ts' },
/--whole-diff builds the diff-reading block alone/,
],
[
// A stray --file on a role that does not read a file would key its record by
// that file, colliding with — and masking — a real file-keyed record.
'reverse-audit + chunk + a stray file',
{ role: 'reverse-audit', chunk: 14, file: 'foo.ts' },
/role "reverse-audit" does not take --file/,
],
[
'all three',
{ chunk: 13, 'whole-diff': true, role: '2' },
/--whole-diff builds the diff-reading block alone/,
],
])('rejects a call that names %s', (_, extra, pattern) => {
// A territory chunk, a named role, or the bare whole-diff block — one primary
// mode. A run that named none used to blame the plan for "no chunk undefined";
// a run that named two would silently pick one. The guard runs before the plan
// is read, so the message is about the call, and it covers every bad shape —
// not just the two the first version tested.
// is read, so the message is about the call, and it names the specific bad shape.
expect(() =>
(agentPromptCommand.handler as (a: unknown) => void)({
plan: '/nonexistent/plan.json',
...extra,
}),
).toThrow(/exactly one of/);
).toThrow(pattern as RegExp);
});

it('accepts --role reverse-audit --chunk N — the one legal role+chunk combo', () => {
// A Step 3B reverse-audit agent owns one chunk's territory. The guard lets that
// one through, and the launch prompt reads exactly that chunk's range — not the
Comment on lines +493 to +495

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This acceptance test calls buildRoleLaunchPrompt directly, bypassing the guard in runAgentPrompt. No test confirms the guard actually allows --role reverse-audit --chunk N through to the prompt-building step.

Failure scenario: A refactor of the guard logic (e.g., back to count-based check) would still pass the rejection tests and this output test, but at runtime runAgentPrompt would reject the one valid combo — and Step 3B reverse-audit would silently never launch.

Consider adding a test through the handler:

expect(() =>
  (agentPromptCommand.handler as (a: unknown) => void)({
    plan: planPath,
    role: 'reverse-audit',
    chunk: 14,
  }),
).not.toThrow();

— qwen3.7-max via Qwen Code /review

// whole diff, which is what makes a large-PR reverse auditor context-starved.
const p = buildRoleLaunchPrompt(PLAN, 'reverse-audit', '/t/ra.brief.md', {
chunk: 14,
});
// Chunk 14 is lines 4025-4200 → offset 4024, limit 176.
expect(p).toContain('offset=4024, limit=176');
// and NOT chunk 13's or chunk 15's range.
expect(p).not.toContain('offset=3807');
});

it('rejects --role reverse-audit --chunk N when the plan has no such chunk', () => {
// The happy path uses chunk 14, which the fixture has. A wrong chunk must name
// what the plan actually holds — not emit offset=NaN, and not credit an empty read.
expect(() =>
buildRoleLaunchPrompt(PLAN, 'reverse-audit', '/t/ra.brief.md', {
chunk: 999,
}),
).toThrow(/the plan has no chunk 999/);
// Through the handler the brief is built first, and rejects it the same way.
const dir = mkdtempSync(join(tmpdir(), 'ap-ra-bad-'));
try {
const plan = join(dir, 'plan.json');
writeFileSync(plan, JSON.stringify(PLAN));
expect(() =>
(agentPromptCommand.handler as (a: unknown) => void)({
plan,
role: 'reverse-audit',
chunk: 999,
}),
).toThrow(/the plan has no chunk 999/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

Expand Down Expand Up @@ -846,3 +966,65 @@ describe('an invariant agent reads its file, not the whole review', () => {
expect(p).toContain('offset=800, limit=400');
});
});

// Step 4 and Step 5 agents: their methodology now lives in code, not in prose the
// orchestrator retypes each run. The rules pinned here are the ones a paraphrase
// would have dropped — and one of them (the documented-intent gate) is the exact
// rule a real run skipped when it auto-posted a false "leaks tokens" Critical.
describe('verify and reverse-audit briefs — the Step 4/5 methodology, in code', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The verify role is tested via buildRoleBrief and buildRoleLaunchPrompt directly, but never through agentPromptCommand.handler. The reverse-audit+chunk handler test covers the handler's I/O path (brief write, record key, prompt record), but a handler-level call with { role: 'verify' } would exercise the output: 'verdicts' branch of tail() through file I/O and confirm the record key is 'verify'.

Concrete cost: the handler's role branch is generic and low-risk, but it is the one new role whose full handler path is not driven end-to-end.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a3e38f4. Added a handler-level test driving { role: "verify" } end-to-end: it does not throw, keys the record verify, and the brief it writes carries the verdict branch of tail() (Exclusion Criteria, no finding format / no Anchor).

it('the verify brief carries the reject-a-Critical high bar and the documented-intent gate', () => {
const p = buildRoleBrief(PLAN, 'verify');
// The verdict is a trace, not a vote.
expect(p).toMatch(/trac(e|ing) it through the real code/i);
// Rejecting a Critical needs quoted contradicting code, floors at low otherwise.
expect(p).toContain('quote the specific code that contradicts');
expect(p).toMatch(/floor is `confirmed \(low confidence\)`/);
// The documented-intent gate — the rule the token-leak false positive skipped.
expect(p).toContain('documented intent');
expect(p).toMatch(/documentation does not make a harm safe/);
// Agent 0 findings are not disproved by a green test.
expect(p).toMatch(/do not reject an issue-fidelity/i);
});

it('the verify brief is a verdict role: Exclusion Criteria yes, finding format no', () => {
const p = buildRoleBrief(PLAN, 'verify');
expect(p).toContain('What is NOT a finding'); // the Exclusion Criteria heading
// It rules on findings; it does not file them, so no finding-format block.
expect(p).not.toContain('**Anchor:**');
});

it('the reverse-audit brief hunts gaps and demands a substantive receipt', () => {
const p = buildRoleBrief(PLAN, 'reverse-audit');
expect(p).toMatch(/find the \*\*gaps\*\*/);
expect(p).toMatch(/Report only Critical or Suggestion/i);
expect(p).toContain('say what you examined'); // the substantive-return receipt
// It DOES file findings, so it keeps the finding format.
expect(p).toContain('**Anchor:**');
});

it('scopes a per-chunk reverse-audit brief to its one chunk, not the whole diff', () => {
// The brief is what the agent is told to obey. If it listed every chunk and said
// "walk it chunk by chunk", a `--chunk 14` auditor would read the whole diff the
// per-chunk design exists to spare it. Its brief reads chunk 14's range alone —
// the same range its launch prompt reads.
const scoped = buildRoleBrief(PLAN, 'reverse-audit', { chunk: 14 });
expect(scoped).toContain('offset=4024, limit=176'); // chunk 14
expect(scoped).not.toContain('offset=3807'); // not chunk 13
expect(scoped).not.toContain('offset=4200'); // not chunk 15
expect(scoped).toContain('chunk 14');
expect(scoped).not.toMatch(/Walk it chunk by chunk/);
// A whole-diff (3A) reverse audit, with no chunk, still walks every chunk.
const whole = buildRoleBrief(PLAN, 'reverse-audit');
expect(whole).toContain('offset=3807');
expect(whole).toContain('offset=4024, limit=176');
expect(whole).toMatch(/Walk it chunk by chunk/);
});

it('both point the agent at its brief file and give it diff reads', () => {
for (const role of ['verify', 'reverse-audit'] as const) {
const launch = buildRoleLaunchPrompt(PLAN, role, `/t/${role}.brief.md`);
expect(launch).toContain(`read_file(file_path="/t/${role}.brief.md")`);
expect(launch).toContain(PLAN.diffPathAbsolute);
}
});
});
Loading
Loading