Checks now self-register through the static barrel (src/checks/all.ts) like the pi base: the ?mtime registry-split workaround is gone, check modules and tests are byte-identical to the base, and registration happens in one module graph instance under omp's extension loader.
163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
/**
|
|
* all-integration.test.ts — `/pygienium-all` end-to-end (task 14).
|
|
*
|
|
* Mirrors the spec scenario: run every registered check in sequence under one
|
|
* resumable run-state and confirm the run completes with every check marked
|
|
* `complete`. Uses the injectable fake agent runner (no model needed) and stub
|
|
* checks whose `!write`/`!echo` task protocol produces deterministic artifacts.
|
|
*/
|
|
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,
|
|
type AgentRunner,
|
|
} from "../src/agent-runner.js";
|
|
import { handleAllCommand, type PygieniumCtx } from "../src/commands.js";
|
|
import { loadRunState } from "../src/run-state.js";
|
|
import { canonicalChecksRoot } from "../src/export.js";
|
|
|
|
/** Stub check whose fake-runner task writes artifacts + echoes a line. */
|
|
function fakeCheck(name: string): CheckDefinition {
|
|
return {
|
|
name,
|
|
label: name,
|
|
description: `${name} check`,
|
|
agentName: "scanner",
|
|
fixAgentName: "fixer",
|
|
phaseId: "scan",
|
|
buildScanTask: (_cwd, scope) =>
|
|
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
|
buildFixTask: (_cwd, _scope, findings) =>
|
|
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
|
gate: () => undefined,
|
|
};
|
|
}
|
|
|
|
function stubCtx(cwd: string): PygieniumCtx {
|
|
return { cwd, mode: "print", hasUI: false, ui: undefined } as PygieniumCtx;
|
|
}
|
|
|
|
/** Capture process.stdout.write lines for the duration of `fn`. */
|
|
async function captureStdout(fn: () => Promise<void>): Promise<string[]> {
|
|
const out: string[] = [];
|
|
const write = process.stdout.write.bind(process.stdout);
|
|
(process.stdout as { write: (chunk: unknown) => boolean }).write = (
|
|
chunk: unknown,
|
|
) => {
|
|
out.push(String(chunk).replace(/\r?\n$/, ""));
|
|
return true;
|
|
};
|
|
try {
|
|
await fn();
|
|
} finally {
|
|
(process.stdout as { write: (chunk: unknown) => boolean }).write = write;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("/pygienium-all end-to-end (task 14)", () => {
|
|
let cwd: string;
|
|
let dispatched: string[];
|
|
let runner: AgentRunner;
|
|
|
|
beforeEach(async () => {
|
|
clearChecks();
|
|
dispatched = [];
|
|
runner = async (opts) => {
|
|
const m = /pygienium\/checks\/([^/]+)\//.exec(opts.task);
|
|
if (m) dispatched.push(m[1] as string);
|
|
return fakeAgentRunner(opts);
|
|
};
|
|
setAgentRunner(runner);
|
|
cwd = await mkdtemp(join(tmpdir(), "pygienium-all-"));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetAgentRunner();
|
|
await rm(cwd, { recursive: true, force: true });
|
|
});
|
|
|
|
it("runs every registered check in sequence and marks the run complete", async () => {
|
|
registerCheck(fakeCheck("alpha"));
|
|
registerCheck(fakeCheck("beta"));
|
|
registerCheck(fakeCheck("gamma"));
|
|
|
|
const out = await captureStdout(() =>
|
|
handleAllCommand("--fix", stubCtx(cwd)),
|
|
);
|
|
|
|
// Every check was dispatched (scan + fix each, in registration order).
|
|
expect(dispatched.filter((n) => n === "alpha").length).toBeGreaterThan(0);
|
|
expect(dispatched.filter((n) => n === "beta").length).toBeGreaterThan(0);
|
|
expect(dispatched.filter((n) => n === "gamma").length).toBeGreaterThan(0);
|
|
|
|
// Final run-state: complete, every check complete, recon shared once.
|
|
const state = await loadRunState(cwd);
|
|
expect(state).toBeDefined();
|
|
expect(state!.status).toBe("complete");
|
|
expect(state!.recon.complete).toBe(true);
|
|
for (const name of ["alpha", "beta", "gamma"]) {
|
|
expect(state!.checks[name]?.status).toBe("complete");
|
|
}
|
|
|
|
// Per-check artifacts landed on disk under the canonical root.
|
|
const alphaFindings = await readFile(
|
|
join(canonicalChecksRoot(cwd), "alpha", "findings.md"),
|
|
"utf8",
|
|
);
|
|
expect(alphaFindings).toContain("alpha findings");
|
|
const gammaChanges = await readFile(
|
|
join(canonicalChecksRoot(cwd), "gamma", "changes.md"),
|
|
"utf8",
|
|
);
|
|
expect(gammaChanges).toContain("gamma changes");
|
|
|
|
// The summary line reports completion and the run-state path.
|
|
const text = out.join("\n");
|
|
expect(text).toContain("pygienium: all-run complete");
|
|
});
|
|
|
|
it("completes cleanly in scan-only mode (no --fix)", async () => {
|
|
registerCheck(fakeCheck("solo"));
|
|
const out = await captureStdout(() => handleAllCommand("", stubCtx(cwd)));
|
|
const state = await loadRunState(cwd);
|
|
expect(state!.status).toBe("complete");
|
|
expect(state!.checks["solo"]?.status).toBe("complete");
|
|
expect(state!.checks["solo"]?.fix).toBe(false);
|
|
// Scan-only still writes findings but not changes.
|
|
const findings = await readFile(
|
|
join(canonicalChecksRoot(cwd), "solo", "findings.md"),
|
|
"utf8",
|
|
);
|
|
expect(findings).toContain("solo findings");
|
|
expect(out.join("\n")).toContain("pygienium: all-run complete");
|
|
});
|
|
|
|
it("reports no checks when the registry is empty", async () => {
|
|
const out = await captureStdout(() => handleAllCommand("", stubCtx(cwd)));
|
|
expect(out.join("\n")).toContain("no checks registered");
|
|
});
|
|
|
|
it("is resumable: a second call reuses the existing run-state", async () => {
|
|
registerCheck(fakeCheck("alpha"));
|
|
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
|
|
const first = await loadRunState(cwd);
|
|
const firstStarted = first!.startedAt;
|
|
|
|
// Second run reloads the existing run-state (same startedAt).
|
|
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
|
|
const second = await loadRunState(cwd);
|
|
expect(second!.startedAt).toBe(firstStarted);
|
|
expect(second!.status).toBe("complete");
|
|
});
|
|
});
|