185 lines
6.4 KiB
TypeScript
185 lines
6.4 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(() => {});
|
|
}
|
|
});
|
|
|
|
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("deep-modules")!;
|
|
|
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
|
|
|
const state = await loadRunState(cwd);
|
|
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
|
});
|
|
});
|