Files
pygienium/tests/deep-modules.test.ts

172 lines
5.8 KiB
TypeScript

/**
* 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,
deepModulesCheck,
} from "../src/checks/deep-modules.js";
function stubCtx(cwd: string): PygieniumCtx {
return { cwd, mode: "print", 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(() => {});
}
});
});