/** * complexity.test.ts — integration tests for the excessive complexity check. * * Tests verify: * 1. A 55-decision-point function (must-refactor band) is refactored * 2. A 40-decision-point function (heavy-skepticism band) is either refactored * or has a documented justification in findings.md * 3. Deep nesting and over-abstraction are simplified */ import { describe, expect, it, beforeEach, afterEach } from "bun:test"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { clearChecks, 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"; /** * A synthetic check that simulates a complexity scan finding two functions: * - one at complexity 55 (must-refactor band) * - one at complexity 40 (skepticism band) */ function synthComplexityCheck(): CheckDefinition { return { name: "synth-complexity", label: "Synth Complexity", description: "Synthetic complexity check for integration tests", agentName: "scanner", fixAgentName: "fixer", phaseId: "scan", buildScanTask: (_cwd, scope) => { const findings = `# complexity — findings ## Cyclomatic complexity | File | Function | Score | Band | Action | |------|----------|-------|------|--------| | target/index.ts:10 | complexFunction | 55 | 50+ | MUST refactor | | target/index.ts:100 | moderateFunction | 40 | 35-49 | Skepticism — justify or refactor | | target/index.ts:200 | simpleFunction | 8 | <35 | OK | ## Structural smells - [high] target/index.ts:15 — deep nesting — 5 levels of nested if/else - [med] target/index.ts:80 — unnecessary wrapper — trivial passthrough function `; // fakeAgentRunner parses one directive per line, so flatten the content // onto a single escaped line; the on-disk file keeps the real text. const oneLine = findings.split("\n").join(" "); return `!write pygienium/checks/synth-complexity/findings.md "${oneLine}"\n!echo ${oneLine}`; }, buildFixTask: (_cwd, _scope, findings) => { const changes = `# complexity — changes\n\n2 refactoring(s) applied; 0 deferred for human review.\n\n## Applied\n\n- target/index.ts:10 — complexFunction split (was 55, now 22, 28)\n- target/index.ts:15 — nested conditionals flattened\n- target/index.ts:80 — trivial wrapper inlined\n\n## Deferred (needs human review)\n\n## Justified (kept at 35–49)\n\n- target/index.ts:100 — moderateFunction (40) — kept: critical routing function on main path, would require major architectural change to split\n`; const oneLine = changes.split("\n").join(" "); return `!write pygienium/checks/synth-complexity/changes.md "${oneLine}"\n!echo ${oneLine}`; }, gate: async (cwd) => { const { stat } = await import("node:fs/promises"); const { resolve } = await import("node:path"); try { const s = await stat(resolve(cwd)); return s.isDirectory() || s.isFile() ? undefined : `target is not a file or directory: ${resolve(cwd)}`; } catch { return `target path does not exist: ${resolve(cwd)}`; } }, }; } function stubCtx(cwd: string): PygieniumCtx { return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx; } describe("complexity check integration", () => { let cwd: string; beforeEach(async () => { clearChecks(); setAgentRunner(fakeAgentRunner); cwd = await mkdtemp(join(tmpdir(), "pygienium-complexity-")); }); afterEach(async () => { resetAgentRunner(); await rm(cwd, { recursive: true, force: true }); }); it("runs a complexity scan and writes findings with cyclomatic scores", async () => { registerCheck(synthComplexityCheck()); const check = synthComplexityCheck(); await handleCheckCommand(check, "", stubCtx(cwd)); const state = await loadRunState(cwd); expect(state).toBeDefined(); expect(state?.checks["synth-complexity"].status).toBe("complete"); // Verify findings contain complexity scores const findings = state?.checks["synth-complexity"].findings; expect(findings).toContain("55"); expect(findings).toContain("40"); expect(findings).toContain("MUST refactor"); }); it("--fix refactors the 50+ function and documents changes", async () => { const check = synthComplexityCheck(); await handleCheckCommand(check, "--fix", stubCtx(cwd)); const state = await loadRunState(cwd); expect(state?.checks["synth-complexity"].status).toBe("complete"); // Verify changes document the refactoring const changes = state?.checks["synth-complexity"].changes; expect(changes).toContain("complexFunction split"); expect(changes).toContain("was 55"); }); it("justified 35-49 functions appear in changes with justification", async () => { const check = synthComplexityCheck(); await handleCheckCommand(check, "--fix", stubCtx(cwd)); const state = await loadRunState(cwd); const changes = state?.checks["synth-complexity"].changes; expect(changes).toContain("Justified"); expect(changes).toContain("moderateFunction"); expect(changes).toContain("critical"); }); it("marks check complete after --fix with no errors", async () => { const check = synthComplexityCheck(); await handleCheckCommand(check, "--fix", stubCtx(cwd)); const state = await loadRunState(cwd); expect(state?.checks["synth-complexity"].error).toBeUndefined(); expect( state?.checks["synth-complexity"].phases.find((p) => p.id === "fix") ?.status, ).toBe("complete"); }); it("gate passes for existing target directory", async () => { const check = synthComplexityCheck(); const gateResult = await check.gate(cwd); expect(gateResult).toBeUndefined(); }); it("gate fails for nonexistent target", async () => { const check = synthComplexityCheck(); const gateResult = await check.gate( "/nonexistent/path/that/does/not/exist", ); expect(gateResult).toContain("does not exist"); }); });