initial import: @mikefreno/omp-pygenium (omp port)
This commit is contained in:
232
tests/defensive-guards.test.ts
Normal file
232
tests/defensive-guards.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* defensive-guards.test.ts — integration test for the defensive-guards check.
|
||||
*
|
||||
* Seeds a temp workspace with:
|
||||
* - noise.ts: a redundant null check on a typed-non-null parameter PLUS a
|
||||
* swallowing try/catch (both redundant);
|
||||
* - boundary.ts: a try/catch around JSON.parse (a legitimate parsing
|
||||
* boundary guard).
|
||||
*
|
||||
* Runs the check with the deterministic fake agent runner and asserts:
|
||||
* - the scan persists findings.md separating redundant guards from boundary
|
||||
* guards;
|
||||
* - with --fix, the redundant guards are removed from noise.ts and changes.md
|
||||
* records them (auto), while the JSON.parse guard in boundary.ts is
|
||||
* preserved untouched (kept — boundary).
|
||||
*/
|
||||
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,
|
||||
check as defensiveGuardsCheck,
|
||||
} from "../src/checks/defensive-guards.js";
|
||||
|
||||
function stubCtx(cwd: string): PygieniumCtx {
|
||||
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `noise.ts`: a redundant null check on a typed-non-null param plus a
|
||||
* swallowing try/catch. Both are redundant — the type system guarantees
|
||||
* `name` is a string, and the catch silently swallows the error.
|
||||
*/
|
||||
async function seedNoise(dir: string): Promise<string> {
|
||||
const noise = join(dir, "noise.ts");
|
||||
await mkdir(dir, { recursive: true }).catch(() => {});
|
||||
await writeFile(
|
||||
noise,
|
||||
[
|
||||
`export function greet(name: string) {`,
|
||||
` if (name === null) return "";`,
|
||||
` return \`hello \${name}\`;`,
|
||||
`}`,
|
||||
``,
|
||||
`export function swallow() {`,
|
||||
` try {`,
|
||||
` doThing();`,
|
||||
` } catch (e) {`,
|
||||
` // swallowed`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
`function doThing() {}`,
|
||||
``,
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
return noise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed `boundary.ts`: a try/catch around JSON.parse of untrusted input. This is
|
||||
* a legitimate parsing boundary guard and must be PRESERVED by --fix.
|
||||
*/
|
||||
async function seedBoundary(dir: string): Promise<string> {
|
||||
const boundary = join(dir, "boundary.ts");
|
||||
await mkdir(dir, { recursive: true }).catch(() => {});
|
||||
await writeFile(
|
||||
boundary,
|
||||
[
|
||||
`export function parse(input: string) {`,
|
||||
` try {`,
|
||||
` return JSON.parse(input);`,
|
||||
` } catch (e) {`,
|
||||
` return null;`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
return boundary;
|
||||
}
|
||||
|
||||
describe("defensive-guards 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(defensiveGuardsCheck);
|
||||
// Fresh empty tempdir per test; each test seeds itself so the skip
|
||||
// test gets a genuinely empty cwd (the gate inspects cwd, not target).
|
||||
cwd = await mkdtemp(join(tmpdir(), "pygienium-dg-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetAgentRunner();
|
||||
await rm(cwd, { recursive: true, force: true }).catch(() => {});
|
||||
});
|
||||
|
||||
it("is registered and uses the defensive-guards scanner agent", () => {
|
||||
const check = getCheck("defensive-guards");
|
||||
expect(check).toBeDefined();
|
||||
expect((check as CheckDefinition)?.agentName).toBe("defensive-guards");
|
||||
});
|
||||
|
||||
it("flags the redundant null check and swallowing try/catch, and keeps the JSON.parse boundary guard, in findings.md", async () => {
|
||||
await seedNoise(cwd);
|
||||
await seedBoundary(cwd);
|
||||
const check = getCheck("defensive-guards")!;
|
||||
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
|
||||
const findings = await readFile(findingsPath(cwd), "utf8");
|
||||
// Redundant guards are flagged with their kind.
|
||||
expect(findings).toContain("noise.ts");
|
||||
expect(findings).toContain("redundant-null-check");
|
||||
expect(findings).toContain("swallowing-try-catch");
|
||||
// The JSON.parse guard is classified as a boundary guard (kept).
|
||||
expect(findings).toContain("boundary.ts");
|
||||
expect(findings).toContain("keep-boundary");
|
||||
expect(findings).toContain("parsing-guard");
|
||||
|
||||
// Run state records the scan summary as findings text.
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||
expect(state?.checks["defensive-guards"]?.findings).toContain(
|
||||
"defensive-guards: 2 redundant",
|
||||
);
|
||||
});
|
||||
|
||||
it("scan-only does not write changes.md and does not touch source files", async () => {
|
||||
const noise = await seedNoise(cwd);
|
||||
const boundary = await seedBoundary(cwd);
|
||||
const before = await readFile(noise, "utf8");
|
||||
const beforeBoundary = await readFile(boundary, "utf8");
|
||||
|
||||
const check = getCheck("defensive-guards")!;
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
|
||||
expect(existsSync(changesPath(cwd))).toBe(false);
|
||||
// Source files untouched by a scan-only run.
|
||||
expect(await readFile(noise, "utf8")).toBe(before);
|
||||
expect(await readFile(boundary, "utf8")).toBe(beforeBoundary);
|
||||
});
|
||||
|
||||
it("--fix removes the redundant guards from noise.ts and records changes.md (auto), and preserves the JSON.parse boundary guard", async () => {
|
||||
const noise = await seedNoise(cwd);
|
||||
const boundary = await seedBoundary(cwd);
|
||||
const check = getCheck("defensive-guards")!;
|
||||
|
||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||
|
||||
const changes = await readFile(changesPath(cwd), "utf8");
|
||||
// Redundant guards: removed (auto).
|
||||
expect(changes).toContain("noise.ts");
|
||||
expect(changes).toMatch(/auto/);
|
||||
expect(changes).toMatch(/redundant-null-check/);
|
||||
expect(changes).toMatch(/swallowing-try-catch/);
|
||||
// JSON.parse boundary guard: kept (boundary — with reason).
|
||||
expect(changes).toContain("boundary.ts");
|
||||
expect(changes).toMatch(/boundary/);
|
||||
expect(changes).toMatch(/JSON.parse/);
|
||||
|
||||
// noise.ts no longer contains the redundant null check or the swallowing
|
||||
// try/catch. The fixer leaves a header marker noting the cleanup.
|
||||
const cleaned = await readFile(noise, "utf8");
|
||||
expect(cleaned).not.toContain("=== null");
|
||||
expect(cleaned).not.toMatch(/try\s*\{/);
|
||||
expect(cleaned).toContain("Cleaned by pygienium-defensive-guards");
|
||||
// The happy-path behaviour is preserved.
|
||||
expect(cleaned).toContain("greet");
|
||||
expect(cleaned).toContain("hello");
|
||||
|
||||
// boundary.ts is PRESERVED — the JSON.parse guard is untouched.
|
||||
const keptBoundary = await readFile(boundary, "utf8");
|
||||
expect(keptBoundary).toContain("JSON.parse");
|
||||
expect(keptBoundary).toMatch(/try\s*\{/);
|
||||
expect(keptBoundary).toMatch(/catch/);
|
||||
// And it still returns null on parse failure (unchanged behaviour).
|
||||
expect(keptBoundary).toContain("return null");
|
||||
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["defensive-guards"]?.fix).toBe(true);
|
||||
expect(state?.checks["defensive-guards"]?.changes).toContain(
|
||||
"2 removed, 1 kept",
|
||||
);
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("findings.md and changes.md live under .pygienium/checks/defensive-guards/", async () => {
|
||||
await seedNoise(cwd);
|
||||
await seedBoundary(cwd);
|
||||
const check = getCheck("defensive-guards")!;
|
||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||
expect(findingsPath(cwd)).toBe(
|
||||
join(cwd, ".pygienium", "checks", "defensive-guards", "findings.md"),
|
||||
);
|
||||
expect(changesPath(cwd)).toBe(
|
||||
join(cwd, ".pygienium", "checks", "defensive-guards", "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("defensive-guards")!;
|
||||
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||
const state = await loadRunState(cwd);
|
||||
expect(state?.checks["defensive-guards"]?.status).toBe("skipped");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user