132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
/**
|
|
* check-runner.test.ts — integration test for the orchestration keystone.
|
|
*
|
|
* Registers a throwaway "smoke" check whose fake sub-agent writes a marker
|
|
* file, then invokes the per-check command handler with a stub context and
|
|
* asserts the run-state marks the check complete and the marker exists.
|
|
* Also asserts `--fix` runs the fix phase and records changes, while
|
|
* scan-only does not.
|
|
*/
|
|
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
import { mkdtemp, readFile, rm } 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, runStatePath } from "../src/run-state.js";
|
|
|
|
function smokeCheck(): CheckDefinition {
|
|
return {
|
|
name: "smoke",
|
|
label: "Smoke",
|
|
description: "Throwaway smoke check for tests",
|
|
agentName: "scanner",
|
|
fixAgentName: "fixer",
|
|
phaseId: "scan",
|
|
buildScanTask: (_cwd, scope) =>
|
|
`!write .pygienium/smoke.marker smoke-complete\n!echo smoke-findings for ${scope.target}`,
|
|
buildFixTask: (_cwd, _scope, findings) =>
|
|
`!echo applied-fixes based on: ${findings.split("\n")[0] ?? ""}`,
|
|
gate: () => undefined,
|
|
};
|
|
}
|
|
|
|
function stubCtx(cwd: string): PygieniumCtx {
|
|
return { cwd, hasUI: false, ui: undefined } as PygieniumCtx;
|
|
}
|
|
|
|
describe("check-runner integration", () => {
|
|
let cwd: string;
|
|
|
|
beforeEach(async () => {
|
|
clearChecks();
|
|
setAgentRunner(fakeAgentRunner);
|
|
cwd = await mkdtemp(join(tmpdir(), "pygienium-smoke-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetAgentRunner();
|
|
await rm(cwd, { recursive: true, force: true });
|
|
});
|
|
|
|
it("runs a smoke check, writes a marker, and marks the check complete", async () => {
|
|
registerCheck(smokeCheck());
|
|
const check = smokeCheck();
|
|
|
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
|
|
|
const state = await loadRunState(cwd);
|
|
expect(state).toBeDefined();
|
|
expect(state?.checks.smoke.status).toBe("complete");
|
|
expect(state?.status).toBe("complete");
|
|
|
|
const marker = await readFile(
|
|
join(cwd, ".pygienium", "smoke.marker"),
|
|
"utf8",
|
|
);
|
|
expect(marker.trim()).toBe("smoke-complete");
|
|
|
|
expect(state?.checks.smoke.findings).toContain("smoke-findings");
|
|
expect(
|
|
state?.checks.smoke.phases.map((p) => `${p.id}=${p.status}`).join(","),
|
|
).toContain("analysis=complete");
|
|
expect(
|
|
state?.checks.smoke.phases.find((p) => p.id === "fix"),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("scan-only does not produce changes and skips the fix phase", async () => {
|
|
const check = smokeCheck();
|
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
|
|
|
const state = await loadRunState(cwd);
|
|
expect(state?.checks.smoke.fix).toBe(false);
|
|
expect(state?.checks.smoke.changes).toBeUndefined();
|
|
const fixPhase = state?.checks.smoke.phases.find((p) => p.id === "fix");
|
|
expect(fixPhase).toBeUndefined();
|
|
});
|
|
|
|
it("--fix runs the fix phase and records changes", async () => {
|
|
const check = smokeCheck();
|
|
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
|
|
|
const state = await loadRunState(cwd);
|
|
expect(state?.checks.smoke.status).toBe("complete");
|
|
expect(state?.checks.smoke.fix).toBe(true);
|
|
expect(state?.checks.smoke.changes).toContain("applied-fixes");
|
|
expect(state?.checks.smoke.phases.find((p) => p.id === "fix")?.status).toBe(
|
|
"complete",
|
|
);
|
|
});
|
|
|
|
it("persists run-state.json at the expected path", async () => {
|
|
const check = smokeCheck();
|
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
|
expect(runStatePath(cwd)).toBe(join(cwd, ".pygienium", "run-state.json"));
|
|
const raw = await readFile(runStatePath(cwd), "utf8");
|
|
expect(JSON.parse(raw).checks.smoke.status).toBe("complete");
|
|
});
|
|
|
|
it("marks a check skipped when the gate returns an error", async () => {
|
|
const gated: CheckDefinition = {
|
|
...smokeCheck(),
|
|
name: "gated",
|
|
label: "Gated",
|
|
gate: () => "no source files matched",
|
|
};
|
|
await handleCheckCommand(gated, "", stubCtx(cwd));
|
|
const state = await loadRunState(cwd);
|
|
expect(state?.checks.gated.status).toBe("skipped");
|
|
expect(state?.checks.gated.error).toBe("no source files matched");
|
|
});
|
|
});
|