Files
ralpi/tests/review-prompt.test.ts
Michael Freno b86174782f feat: add per-file +/− summary and noise-filtered scope to review prompts
Emit a ### Changed Files Markdown table (| File | +/− | Type | rows plus
total added/removed) ahead of the raw diff in both committed and
uncommitted review prompts, parsed from the diff via the shared
parseDiff engine. Add an ### Excluded Files (n) section listing filtered
noise (path, +/− counts, reason), and replace byte-truncation of oversized
diffs with a file-list + read instruction when the cleaned diff exceeds
50KB or touches more than 20 files.

Also: distinguish diff-computation failure from genuinely no changes in the
review loop (tri-state result, never treats a broken base ref as a clean
verified task), map critical→blocker in finding severity parsing, inject a
per-review custom focus/instructions section from config, and make the
noise-filter ignore rules project-configurable via review.extraIgnorePatterns
and review.ignorePaths. Add same-model retry before cycling to the next
model in task/follow-up/fix sessions.
2026-08-08 18:10:08 -04:00

260 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Tests for the review prompt builders (src/prompts.ts).
* Covers: per-file summary table, excluded-files section, oversized-diff
* read-instruction (never byte-truncates), custom review focus, and the
* configurable noise-filter overrides surfacing in the prompt.
*/
import { describe, test, expect } from "bun:test";
import { buildReviewPrompt, buildReviewPromptUncommitted } from "../src/prompts";
import { compileIgnorePatterns } from "../src/diff";
import type { Task, Project } from "../src/types";
const task: Task = {
id: "01",
title: "Implement auth",
description: "Add a login flow",
status: "completed",
dependencies: [],
};
const project: Project = {
objective: "Build the app",
sourcePath: "README.md",
sourceDir: "/tmp",
tasks: [task],
dependencies: {},
};
/** A diff mixing one code file plus lockfile/minified/binary noise. */
const MIXED_DIFF = [
"diff --git a/src/auth.ts b/src/auth.ts",
"index 111..222 100644",
"--- a/src/auth.ts",
"+++ b/src/auth.ts",
"@@ -1,3 +1,5 @@",
" import { hash } from \"./hash\";",
"+export function login() {",
"+ return hash(secret);",
"- return legacy();",
"+}",
"",
"diff --git a/package-lock.json b/package-lock.json",
"index 000..111 100644",
"--- a/package-lock.json",
"+++ b/package-lock.json",
"@@ -0,0 +1,3 @@",
"+{",
'+ "name": "x"',
"+}",
"",
"diff --git a/assets/logo.png b/assets/logo.png",
"index 111..222 100644",
"Binary files differ",
"",
"diff --git a/dist/app.min.js b/dist/app.min.js",
"index 111..222 100644",
"--- a/dist/app.min.js",
"+++ b/dist/app.min.js",
"@@ -1 +1 @@",
"-var a=1;",
"+var a=2;",
].join("\n");
function manyFileDiff(n: number): string {
const chunks: string[] = [];
for (let i = 0; i < n; i++) {
chunks.push(
`diff --git a/src/f${String(i).padStart(2, "0")}.ts b/src/f${String(i).padStart(2, "0")}.ts`,
"--- a/src/f.ts",
"+++ b/src/f.ts",
`+line ${i}`,
);
}
return chunks.join("\n");
}
describe("buildReviewPrompt", () => {
test("emits a per-file +/ summary table with totals, excluding noise", () => {
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
);
expect(prompt).toContain("### Changed Files");
expect(prompt).toContain("| `src/auth.ts` | +3/-1 | ts |");
expect(prompt).toContain("| **Total** | **+3/-1** | |");
});
test("surfaces an excluded-files section with path, counts, and reason", () => {
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
);
expect(prompt).toContain("### Excluded Files (3)");
expect(prompt).toContain("- `package-lock.json` (+3/-0) — lockfile");
expect(prompt).toContain("- `assets/logo.png` (+0/-0) — binary/media asset");
expect(prompt).toContain("- `dist/app.min.js` (+1/-1) — minified asset");
});
test("never inlines excluded (noise) chunks into the diff block", () => {
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
);
// The noise chunks themselves are never inlined — only the excluded-files
// section names them (as `- path (+x/-y) — reason`, no `diff --git` header).
expect(prompt).not.toContain("diff --git a/package-lock.json");
expect(prompt).not.toContain("diff --git a/assets/logo.png");
expect(prompt).not.toContain("diff --git a/dist/app.min.js");
// The cleaned diff block is present with the code file.
expect(prompt).toContain("```diff");
expect(prompt).toContain("diff --git a/src/auth.ts");
});
test("omits the excluded section entirely when nothing is excluded", () => {
const clean = [
"diff --git a/src/auth.ts b/src/auth.ts",
"--- a/src/auth.ts",
"+++ b/src/auth.ts",
"+export const x = 1;",
].join("\n");
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
clean,
);
expect(prompt).not.toContain("### Excluded Files");
expect(prompt).toContain("| `src/auth.ts` | +1/-0 | ts |");
});
test("switches to a file-list + read instruction for >20 files, no truncation", () => {
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: many",
manyFileDiff(21),
);
expect(prompt).toContain("Diff too large");
expect(prompt).toContain("Use `read` to inspect the changed files");
// No byte-truncated inline diff for oversized inputs.
expect(prompt).not.toContain("```diff");
});
test("inlines a small diff normally (no read-instruction)", () => {
const prompt = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
);
expect(prompt).not.toContain("Diff too large");
});
test("emits a Custom Review Focus section only when focus is set", () => {
const withFocus = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
{ focus: "check security only" },
);
expect(withFocus).toContain("## Custom Review Focus");
expect(withFocus).toContain("check security only");
const withoutFocus = buildReviewPrompt(
task,
project,
"abc1234",
"feat: auth",
MIXED_DIFF,
);
expect(withoutFocus).not.toContain("## Custom Review Focus");
});
test("surfaces extra ignore patterns and ignorePaths overrides in the prompt", () => {
const diff = [
"diff --git a/src/keep.ts b/src/keep.ts",
"--- a/src/keep.ts",
"+++ b/src/keep.ts",
"+keep",
"diff --git a/package-lock.json b/package-lock.json",
"--- a/package-lock.json",
"+++ b/package-lock.json",
"+a",
"+b",
"+c",
"+d",
].join("\n");
// ignorePaths keeps the lockfile in scope → it shows in the table,
// and no excluded section is emitted.
const kept = buildReviewPrompt(task, project, "abc1234", "x", diff, {
diffOptions: { ignorePaths: ["package-lock.json"] },
});
expect(kept).toContain("| `package-lock.json` | +4/-0 | json |");
expect(kept).not.toContain("### Excluded Files");
// Without ignorePaths, the lockfile is excluded.
const excluded = buildReviewPrompt(task, project, "abc1234", "x", diff);
expect(excluded).not.toContain("| `package-lock.json` |");
expect(excluded).toContain("### Excluded Files (1)");
// extraPatterns drops a matching file from scope.
const dropped = buildReviewPrompt(task, project, "abc1234", "x", diff, {
diffOptions: {
extraPatterns: compileIgnorePatterns(["\\.ts$"]),
ignorePaths: [],
},
});
expect(dropped).not.toContain("| `src/keep.ts` |");
expect(dropped).toContain("### Excluded Files (2)");
});
});
describe("buildReviewPromptUncommitted", () => {
test("emits summary table, excluded section, and cleaned diff", () => {
const prompt = buildReviewPromptUncommitted(
task,
project,
"M src/auth.ts",
MIXED_DIFF,
);
expect(prompt).toContain("### Changed Files");
expect(prompt).toContain("| `src/auth.ts` | +3/-1 | ts |");
expect(prompt).toContain("### Excluded Files (3)");
expect(prompt).not.toContain("diff --git a/package-lock.json");
expect(prompt).toContain("### Current Tracked Diff (git diff)");
});
test("supports custom focus", () => {
const prompt = buildReviewPromptUncommitted(
task,
project,
"M src/auth.ts",
MIXED_DIFF,
{ focus: "review performance" },
);
expect(prompt).toContain("## Custom Review Focus");
expect(prompt).toContain("review performance");
});
});