Files
pygienium/tests/todos.test.ts
Michael Freno 63c2f73a5e
All checks were successful
port-to-omp / port (push) Successful in 6s
better recon info in certain (nested) situations
2026-08-12 21:59:49 -04:00

396 lines
12 KiB
TypeScript

/**
* todos.test.ts — unit + integration tests for the todos (TODOs & stubs) check.
*
* Unit: `detectTodoStubs` over a multi-language fixture tree — markers, silent
* stubs (placeholder return / empty body / pass-only body), loud stubs, and
* the negative cases (a real adder, a `return null` catch handler, in-string
* "TODO" flagged for recall, clean code never flagged).
*
* Integration (deterministic fake agent runner): the scan persists findings.md
* with the three sections + machine-readable summary and a new/resolved delta
* vs the previous run; --fix converts silent stubs to loud throws, preserves
* markers, leaves loud stubs untouched, and records changes.md.
*/
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
import { existsSync } from "node:fs";
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
clearChecks,
getCheck,
registerCheck,
type CheckDefinition,
} from "../src/checks/registry.js";
import {
setAgentRunner,
resetAgentRunner,
fakeAgentRunner,
} from "../src/agent-runner.js";
import { handleCheckCommand, type PygieniumCtx } from "../src/commands.js";
import { loadRunState } from "../src/run-state.js";
import {
findingsPath,
changesPath,
todosCheck,
detectTodoStubs,
todosPriorCounts,
buildTodosScanTask,
} from "../src/checks/todos.js";
function stubCtx(cwd: string): PygieniumCtx {
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
}
/**
* Seed `stubs.ts` with the full taxonomy: a TODO marker, a silent stub with a
* placeholder return (getPrice), a silent stub with an empty body (notify),
* and a loud stub (connect throws "Not implemented").
*/
async function seedStubs(dir: string): Promise<void> {
await mkdir(dir, { recursive: true }).catch(() => {});
await writeFile(
join(dir, "stubs.ts"),
[
`// TODO: add pagination`,
`export function getPrice(): number {`,
` return 0;`,
`}`,
``,
`export function notify(): void {}`,
``,
`export function connect(): Promise<void> {`,
` throw new Error("Not implemented");`,
`}`,
``,
].join("\n"),
"utf8",
);
}
/** Seed the multi-language fixture tree for the deterministic-detector tests. */
async function seedTree(dir: string): Promise<void> {
await mkdir(dir, { recursive: true }).catch(() => {});
await Promise.all([
writeFile(
join(dir, "math.ts"),
"export function add(a: number, b: number): number {\n return a + b;\n}\n",
"utf8",
),
writeFile(
join(dir, "parse.ts"),
[
`export function parse(input: string) {`,
` try {`,
` return JSON.parse(input);`,
` } catch {`,
` return null;`,
` }`,
`}`,
``,
].join("\n"),
"utf8",
),
writeFile(join(dir, "stringlit.ts"), 'export const op = "TODO";\n', "utf8"),
writeFile(
join(dir, "stubs.ts"),
[
`// TODO: add pagination`,
`export function getPrice(): number {`,
` return 0;`,
`}`,
``,
`export function notify(): void {}`,
``,
`export function connect(): Promise<void> {`,
` throw new Error("Not implemented");`,
`}`,
``,
].join("\n"),
"utf8",
),
writeFile(
join(dir, "repo.py"),
[
`class Repository:`,
` def find(self, uid):`,
` raise NotImplementedError # interface method`,
``,
` def save(self, record):`,
` pass`,
``,
].join("\n"),
"utf8",
),
writeFile(
join(dir, "fetch.rs"),
"fn fetch() -> Result<u32, String> {\n todo!()\n}\n",
"utf8",
),
]);
}
describe("detectTodoStubs", () => {
let dir: string;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "pygienium-todos-unit-"));
await seedTree(dir);
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true }).catch(() => {});
});
it("flags markers, silent stubs, and loud stubs across languages", async () => {
const hits = await detectTodoStubs(dir);
const marker = hits.filter((h) => h.kind === "marker");
const silent = hits.filter((h) => h.kind === "silent-stub");
const loud = hits.filter((h) => h.kind === "loud-stub");
// Clean code is never flagged: the adder and the `return null` catch
// handler (a boundary handler, not a stub) produce zero hits.
expect(
hits.filter((h) => /(?:math\.ts|parse\.ts)$/.test(h.path)),
).toHaveLength(0);
// Markers: the stubs.ts TODO comment, and the in-string "TODO" (recall —
// the scan agent's job is to drop the string-literal noise).
expect(
marker.some(
(h) =>
h.path.endsWith("stubs.ts") && h.line === 1 && h.snippet === "TODO",
),
).toBe(true);
expect(
marker.some(
(h) => h.path.endsWith("stringlit.ts") && h.snippet === "TODO",
),
).toBe(true);
// Silent stubs: lone placeholder return + empty body in stubs.ts,
// pass-only body in repo.py — each with its enclosing function.
const stubsSilent = silent.filter((h) => h.path.endsWith("stubs.ts"));
expect(
stubsSilent.some(
(h) => h.snippet === "placeholder-return" && h.context === "getPrice",
),
).toBe(true);
expect(
stubsSilent.some(
(h) => h.snippet === "empty-body" && h.context === "notify",
),
).toBe(true);
expect(
silent.some(
(h) =>
h.path.endsWith("repo.py") &&
h.snippet === "pass-only" &&
h.context === "save",
),
).toBe(true);
// Loud stubs: "Not implemented" throw, raise NotImplementedError,
// rust todo!() — reported as tracked debt.
expect(
loud.some(
(h) => h.path.endsWith("stubs.ts") && h.snippet === "Not implemented",
),
).toBe(true);
expect(
loud.some(
(h) =>
h.path.endsWith("repo.py") && h.snippet === "NotImplementedError",
),
).toBe(true);
expect(
loud.some((h) => h.path.endsWith("fetch.rs") && h.snippet === "todo!("),
).toBe(true);
});
it("never descends into build/deploy output directories", async () => {
// Generated bundles under framework build dirs must not feed the
// pre-scan: they dominate candidate counts with minified noise (the
// freno-dev failure flagged 119 of 124 candidates inside
// `.output`/`.vercel` bundles, ballooning the scan task to 2.5 MB).
for (const rel of [
join(".output", "public", "bundle.js"),
join(".vercel", "output", "static", "app.js"),
join(".netlify", "functions", "bundle.js"),
]) {
const full = join(dir, rel);
await mkdir(join(full, ".."), { recursive: true });
await writeFile(
full,
"// TODO: bundle placeholder\nfunction f(){ return 0; }\nthrow new Error('not implemented');\n",
"utf8",
);
}
const hits = await detectTodoStubs(dir);
expect(
hits.filter((h) => /(?:\.output|\.vercel|\.netlify)[/\\]/.test(h.path)),
).toHaveLength(0);
});
});
describe("todos check", () => {
let cwd: string;
beforeEach(async () => {
clearChecks();
setAgentRunner(fakeAgentRunner);
// Re-register explicitly: the module's import-time registerCheck only
// runs once (module cache), so clearChecks + registerCheck restores it
// deterministically for each test.
registerCheck(todosCheck);
cwd = await mkdtemp(join(tmpdir(), "pygienium-todos-"));
});
afterEach(async () => {
resetAgentRunner();
await rm(cwd, { recursive: true, force: true }).catch(() => {});
});
it("is registered and uses the todos scanner agent", () => {
const check = getCheck("todos");
expect(check).toBeDefined();
expect((check as CheckDefinition)?.agentName).toBe("todos");
});
it("scan-only writes findings.md with the three sections and leaves sources untouched", async () => {
await seedStubs(cwd);
const check = getCheck("todos")!;
await handleCheckCommand(check, "", stubCtx(cwd));
const findings = await readFile(findingsPath(cwd), "utf8");
expect(findings).toContain(
"summary: 1 marker(s), 2 silent stub(s), 1 loud stub(s)",
);
expect(findings).toContain("new: 4"); // first run: everything is new
expect(findings).toContain("## TODO markers");
expect(findings).toContain("## Silent stubs (actionable)");
expect(findings).toContain("placeholder-return");
expect(findings).toContain("Not implemented");
// Scan-only: no changes.md, sources untouched.
expect(existsSync(changesPath(cwd))).toBe(false);
const untouched = await readFile(join(cwd, "stubs.ts"), "utf8");
expect(untouched).toContain("return 0;");
// Run state records the scan summary as findings text.
const state = await loadRunState(cwd);
expect(state?.checks["todos"]?.status).toBe("complete");
expect(state?.checks["todos"]?.findings).toContain(
"todos: 2 silent stub(s), 1 loud stub(s), 1 marker(s)",
);
});
it("--fix converts silent stubs to loud throws, preserves markers and loud stubs, records changes.md", async () => {
await seedStubs(cwd);
const check = getCheck("todos")!;
await handleCheckCommand(check, "--fix", stubCtx(cwd));
const cleaned = await readFile(join(cwd, "stubs.ts"), "utf8");
// Silent stubs now throw loudly, naming the function.
expect(cleaned).toContain('throw new Error("todos: getPrice() is a stub")');
expect(cleaned).toContain('throw new Error("todos: notify() is a stub")');
// Markers are NEVER deleted and loud stubs are NEVER touched.
expect(cleaned).toContain("// TODO: add pagination");
expect(cleaned).toContain('throw new Error("Not implemented")');
expect(cleaned).toContain("Cleaned by pygienium-todos");
// The placeholder bodies are gone.
expect(cleaned).not.toContain("return 0;");
expect(cleaned).not.toContain("notify(): void {}");
const changes = await readFile(changesPath(cwd), "utf8");
expect(changes).toContain(
"summary: 2 silent stub(s) converted to loud, 0 kept",
);
expect(changes).toContain("getPrice()");
expect(changes).toContain("notify()");
const state = await loadRunState(cwd);
expect(state?.checks["todos"]?.fix).toBe(true);
expect(state?.checks["todos"]?.status).toBe("complete");
});
it("findings.md and changes.md live under .pygienium/checks/todos/", async () => {
await seedStubs(cwd);
const check = getCheck("todos")!;
await handleCheckCommand(check, "--fix", stubCtx(cwd));
expect(findingsPath(cwd)).toBe(
join(cwd, ".pygienium", "checks", "todos", "findings.md"),
);
expect(changesPath(cwd)).toBe(
join(cwd, ".pygienium", "checks", "todos", "changes.md"),
);
});
it("skips when the target has no source files", async () => {
// cwd is a fresh empty tempdir (no seeding) → the gate finds no source
// files and skips the check without spawning an agent.
const check = getCheck("todos")!;
await handleCheckCommand(check, "", stubCtx(cwd));
const state = await loadRunState(cwd);
expect(state?.checks["todos"]?.status).toBe("skipped");
});
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
// Top level holds only a directory — the old shallow gate skipped any
// repo whose source lives under `game/`/`src/`-style subdirs.
await mkdir(join(cwd, "game"), { recursive: true });
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
const check = getCheck("todos")!;
await handleCheckCommand(check, "", stubCtx(cwd));
const state = await loadRunState(cwd);
expect(state?.checks["todos"]?.status).toBe("complete");
});
it("reports a new/resolved delta against the previous run's counts", async () => {
await seedStubs(cwd);
const check = getCheck("todos")!;
await handleCheckCommand(check, "", stubCtx(cwd));
// The previous run's verified counts are parseable from run-state.
const prior = await todosPriorCounts(cwd);
expect(prior).toEqual({ silent: 2, loud: 1, marker: 1 });
// Nobody flagged anything new and everything was resolved: the next scan
// sees zero candidates → new: 0, resolved: 4 (all prior items).
await rm(join(cwd, "stubs.ts"));
const task = await buildTodosScanTask(cwd, {
cwd,
target: cwd,
fix: false,
rest: [],
});
expect(task).toContain(
"summary: 0 marker(s), 0 silent stub(s), 0 loud stub(s)",
);
expect(task).toContain("| new: 0 | resolved: 4 |");
});
it("truncates giant single-line candidates so the task prompt stays bounded", async () => {
// A minified/generated single line can be hundreds of KB; embedding it
// wholesale ballooned the freno-dev task to 2.5 MB and choked the
// analysis agent. The task must carry a truncated prefix, never the
// full line.
const long = `// TODO: ${"x".repeat(400)}`;
await writeFile(
join(cwd, "huge.ts"),
`${long}\nexport function f() { return 0; }\n`,
"utf8",
);
const task = await buildTodosScanTask(cwd, {
cwd,
target: cwd,
fix: false,
rest: [],
});
expect(task).not.toContain("x".repeat(400));
expect(task).toContain("…");
});
});