/** * deep-modules.test.ts — integration test for the deep-modules check. * * Seeds a temp workspace with a pass-through wrapper module (a shallow * abstraction), runs the check with the deterministic fake agent runner, and * asserts: * - the scan persists `findings.md` flagging the wrapper as a pass-through; * - with `--fix`, the safe consolidation is applied (wrapper rewritten/ * removed) and `changes.md` records it (auto), while a risky * external-importer case is listed for review (manual), never auto-applied. */ 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 deepModulesCheck, } from "../src/checks/deep-modules.js"; function stubCtx(cwd: string): PygieniumCtx { return { cwd, hasUI: false, ui: undefined } as PygieniumCtx; } /** Drop a pass-through wrapper module that forwards a single lib call. */ async function seedPassThrough( dir: string, ): Promise<{ wrapper: string; lib: string }> { const wrapper = join(dir, "wrapper.ts"); const lib = join(dir, "lib.ts"); await mkdir(dir, { recursive: true }).catch(() => {}); await writeFile( lib, `export function compute(x: number): number { return x * 2; }\n`, "utf8", ); // Shallow pass-through: forwards every argument to `lib` with zero added logic. await writeFile( wrapper, `import { compute } from "./lib";\nexport function run(x: number) { return compute(x); }\n`, "utf8", ); return { wrapper, lib }; } describe("deep-modules 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(deepModulesCheck); cwd = await mkdtemp(join(tmpdir(), "pygienium-deep-")); }); afterEach(async () => { resetAgentRunner(); await rm(cwd, { recursive: true, force: true }).catch(() => {}); }); it("is registered and uses the deep-modules scanner agent", () => { const check = getCheck("deep-modules"); expect(check).toBeDefined(); expect((check as CheckDefinition)?.agentName).toBe("deep-modules"); }); it("flags the pass-through wrapper in findings.md", async () => { await seedPassThrough(cwd); const check = getCheck("deep-modules")!; await handleCheckCommand(check, "", stubCtx(cwd)); const findings = await readFile(findingsPath(cwd), "utf8"); expect(findings).toContain("wrapper.ts"); expect(findings).toContain("pass-through-wrapper"); expect(findings).toMatch(/importers:\s*0/); // Run state records the scan summary as findings text. const state = await loadRunState(cwd); expect(state?.checks["deep-modules"]?.status).toBe("complete"); expect(state?.checks["deep-modules"]?.findings).toContain( "deep-modules: 1 issue", ); }); it("scan-only does not write changes.md", async () => { await seedPassThrough(cwd); const check = getCheck("deep-modules")!; await handleCheckCommand(check, "", stubCtx(cwd)); expect(existsSync(changesPath(cwd))).toBe(false); }); it("--fix applies the safe consolidation and records changes.md (auto), and defers the risky one (manual)", async () => { await seedPassThrough(cwd); // Also drop a "risky" adapter so we can assert it is NOT auto-applied. await writeFile( join(cwd, "risky-adapter.ts"), `// adapter-layer with external importers — should be listed for review only\nexport const risky = true;\n`, "utf8", ); const check = getCheck("deep-modules")!; await handleCheckCommand(check, "--fix", stubCtx(cwd)); const changes = await readFile(changesPath(cwd), "utf8"); // Safe pass-through: consolidation applied (auto). expect(changes).toContain("wrapper.ts"); expect(changes).toMatch(/auto/); expect(changes).toMatch(/consolidat/i); // Risky adapter: listed for review, not auto-applied (manual). expect(changes).toContain("risky-adapter.ts"); expect(changes).toMatch(/manual/); // The safe wrapper was rewritten — no longer a pass-through. const wrapperContent = await readFile(join(cwd, "wrapper.ts"), "utf8"); expect(wrapperContent).not.toContain("import { compute }"); expect(wrapperContent).toContain("Consolidated"); const state = await loadRunState(cwd); expect(state?.checks["deep-modules"]?.fix).toBe(true); expect(state?.checks["deep-modules"]?.changes).toContain( "1 auto-applied, 1 deferred", ); expect(state?.checks["deep-modules"]?.status).toBe("complete"); }); it("findings.md and changes.md live under .pygienium/checks/deep-modules/", async () => { await seedPassThrough(cwd); const check = getCheck("deep-modules")!; await handleCheckCommand(check, "--fix", stubCtx(cwd)); expect(findingsPath(cwd)).toBe( join(cwd, ".pygienium", "checks", "deep-modules", "findings.md"), ); expect(changesPath(cwd)).toBe( join(cwd, ".pygienium", "checks", "deep-modules", "changes.md"), ); }); it("skips when the target has no source files", async () => { const empty = await mkdtemp(join(tmpdir(), "pygienium-empty-")); try { const check = getCheck("deep-modules")!; await handleCheckCommand(check, empty, stubCtx(cwd)); const state = await loadRunState(cwd); expect(state?.checks["deep-modules"]?.status).toBe("skipped"); } finally { await rm(empty, { recursive: true, force: true }).catch(() => {}); } }); });