Files
pygienium/tests/all.test.ts
Michael Freno 8990a94321 fix: a finished all-run is not a run to resume
`/pygienium-all` loaded the previous run-state and skipped every check
whose entry was terminal, so a second invocation dispatched nothing at
all. The one check that did run was whichever an interrupted run had
left non-terminal — registry order made that `comments`, which read as
"comments ran, everything after it was skipped".

Resumability belongs to unfinished work: continue only when the stored
run is not complete (`in_progress`, `partial`, `failed`), so completed
checks are carried forward cheaply and the interrupted one picks up
where it stopped. A completed run means the next invocation is a new
scan, which is what the command name promises; `--fresh` and
`/pygienium-resume` keep their explicit jobs.

Settle a terminal check's phases on the way past too: an interrupted run
can leave a phase `in_progress` behind a check that reads `complete`,
which made the state claim both finished and still running.

Tests: reverted src/ fails "a completed run is not resumed", "settles a
phase an interrupted run left in_progress", and the integration test "an
unfinished run-state is continued, not restarted"; all pass with the
fix. Suite 164 pass, 1 pre-existing worktree-only failure (MISC-46).
bun run typecheck clean; port-to-omp regenerates and typechecks.

MISC-47

Co-Authored-By: nw-flash-flex <noreply@neuralwatt.com>
2026-10-20 23:29:01 -04:00

464 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* all.test.ts — integration test for the `/pygienium-all` orchestrator (task 12).
*
* Mirrors the spec scenario: run `/pygienium-all` on a small repo and assert:
* - every registered check runs exactly once in registry order;
* - run-state shows all checks complete and the overall run complete;
* - `.pygienium/all-summary.md` is present and lists per-check outcomes;
* - `--only=alpha,gamma` narrows the candidate set preserving order;
* - interrupted/resumed runs re-dispatch non-terminal checks while skipping
* terminal ones, unless `--fresh` resets everything.
*
* A tracker wraps the fake agent runner so we can assert dispatch order and
* counts without a model.
*/
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
import { mkdtemp, mkdir, 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 {
parseAllArgs,
runAllChecks,
allSummaryPath,
renderAllSummary,
selectChecks,
} from "../src/modes/all.js";
import {
loadRunState,
markCheckStatus,
applyPhaseStatus,
initRunState,
markRunStatus,
saveRunState,
PHASE_RECON,
PHASE_ANALYSIS,
PHASE_FIX,
PHASE_VERIFY,
PHASE_CLEANUP,
} from "../src/run-state.js";
import { writeFile } from "node:fs/promises";
/** Build a deterministic check whose fake runner writes on-disk artifacts. */
function fakeCheck(name: string): CheckDefinition {
return {
name,
label: name.charAt(0).toUpperCase() + name.slice(1),
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;
}
/** Tracker: records dispatched agent tasks then delegates to the fake runner. */
function trackingRunner(): { runner: AgentRunner; dispatched: string[] } {
const dispatched: string[] = [];
const runner: AgentRunner = async (opts) => {
const m = /pygienium\/checks\/([^/]+)\//.exec(opts.task);
if (m) dispatched.push(m[1] as string);
return fakeAgentRunner(opts);
};
return { runner, dispatched };
}
/** 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;
}
/** Mark a check as fully complete in the run-state (helper for seeding). */
function markComplete(
state: Parameters<typeof markCheckStatus>[0],
name: string,
): void {
for (const phaseId of [
PHASE_RECON,
PHASE_ANALYSIS,
PHASE_FIX,
PHASE_VERIFY,
PHASE_CLEANUP,
]) {
applyPhaseStatus(state, name, phaseId, "complete");
}
markCheckStatus(state, name, "complete");
}
describe("/pygienium-all orchestrator (task 12)", () => {
let cwd: string;
let track: ReturnType<typeof trackingRunner>;
beforeEach(async () => {
clearChecks();
track = trackingRunner();
setAgentRunner(track.runner);
cwd = await mkdtemp(join(tmpdir(), "pygienium-all-"));
// Seed a source file so the gate passes and recon has something to scan.
await mkdir(join(cwd, "src"), { recursive: true });
});
afterEach(async () => {
resetAgentRunner();
await rm(cwd, { recursive: true, force: true });
});
it("parseAllArgs parses path, --fix, --fresh, --no-gitignore, and --only", () => {
const p = parseAllArgs("subdir --fix --only=alpha,beta --fresh", cwd);
expect(p.target).toBe(join(cwd, "subdir"));
expect(p.fix).toBe(true);
expect(p.fresh).toBe(true);
expect(p.gitignore).toBe(true); // default: keep the .gitignore guard on
expect(p.only).toEqual(["alpha", "beta"]);
const noGi = parseAllArgs("--no-gitignore", cwd);
expect(noGi.gitignore).toBe(false);
expect(noGi.target).toBe(cwd);
});
it("selectChecks preserves registry order for the --only subset", () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
registerCheck(fakeCheck("gamma"));
const subset = selectChecks(["gamma", "alpha"]); // order in --only is irrelevant
expect(subset.map((c) => c.name)).toEqual(["alpha", "gamma"]);
expect(selectChecks().length).toBe(3);
expect(selectChecks([]).length).toBe(3);
});
it("runs every registered check exactly once in registry order", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
registerCheck(fakeCheck("gamma"));
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
// Each check dispatched once for scan + once for fix (3 checks × 2 phases).
const scanDispatches = track.dispatched.filter((n) => n !== undefined);
expect(scanDispatches).toEqual([
"alpha",
"alpha",
"beta",
"beta",
"gamma",
"gamma",
]);
const state = await loadRunState(cwd);
expect(state?.status).toBe("complete");
expect(state?.checks.alpha.status).toBe("complete");
expect(state?.checks.beta.status).toBe("complete");
expect(state?.checks.gamma.status).toBe("complete");
expect(state?.recon.complete).toBe(true);
});
it("writes .pygienium/all-summary.md listing per-check outcomes", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
const summary = await readFile(allSummaryPath(cwd), "utf8");
expect(summary).toContain("# Pygienium all-run summary");
expect(summary).toContain("- status: complete");
expect(summary).toContain("## alpha — complete");
expect(summary).toContain("## beta — complete");
expect(summary).toContain("## alpha — complete (--fix)");
// Artifact paths + line counts are referenced.
expect(summary).toContain("findings:");
expect(summary).toContain("changes:");
// Artifacts actually exist on disk.
const alphaFindings = await readFile(
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
"utf8",
);
expect(alphaFindings).toContain("alpha findings");
});
it("--only narrows the run to the named subset", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
registerCheck(fakeCheck("gamma"));
await captureStdout(() =>
handleAllCommand("--only=alpha,gamma", stubCtx(cwd)),
);
// Only alpha + gamma dispatched (beta never touched).
expect(track.dispatched).toContain("alpha");
expect(track.dispatched).toContain("gamma");
expect(track.dispatched).not.toContain("beta");
const state = await loadRunState(cwd);
expect(state?.checks.alpha.status).toBe("complete");
expect(state?.checks.gamma.status).toBe("complete");
// beta was not part of the selected set, so has no entry.
expect(state?.checks.beta).toBeUndefined();
const summary = await readFile(allSummaryPath(cwd), "utf8");
expect(summary).toContain("## alpha");
expect(summary).toContain("## gamma");
expect(summary).not.toContain("## beta");
});
it("a completed run is not resumed: the next invocation runs every check again", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
// First run: both complete.
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
expect(track.dispatched).toEqual(["alpha", "alpha", "beta", "beta"]);
// Second run, no flags: the stored run completed, so this is a new scan.
// Every check dispatches again — the regression this pins dispatched
// nothing but whichever check an interrupted run happened to leave open.
track.dispatched.length = 0;
const out = await captureStdout(() =>
handleAllCommand("--fix", stubCtx(cwd)),
);
expect(track.dispatched).toEqual(["alpha", "alpha", "beta", "beta"]);
expect(out.join("\n")).not.toContain("skipping");
const state = await loadRunState(cwd);
expect(state?.status).toBe("complete");
expect(state?.checks.alpha?.status).toBe("complete");
expect(state?.checks.beta?.status).toBe("complete");
// --fresh remains the explicit reset and dispatches the same way.
track.dispatched.length = 0;
await captureStdout(() => handleAllCommand("--fix --fresh", stubCtx(cwd)));
expect(track.dispatched).toEqual(["alpha", "alpha", "beta", "beta"]);
});
it("resumes an unfinished run: terminal checks skip, the rest re-dispatch", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
// A run that stopped with alpha done and beta failed is unfinished work,
// so it continues instead of starting over.
const state = initRunState(cwd, [
{ name: "alpha", label: "alpha", fix: true },
{ name: "beta", label: "beta", fix: true },
]);
state.recon = {
complete: true,
path: join(cwd, ".pygienium", "recon.json"),
finishedAt: Date.now(),
};
markComplete(state, "alpha");
markCheckStatus(state, "beta", "failed", "provider reset mid-scan");
markRunStatus(state, "partial");
await saveRunState(state);
const out = await captureStdout(() =>
handleAllCommand("--fix", stubCtx(cwd)),
);
expect(track.dispatched).toEqual(["beta", "beta"]);
expect(out.join("\n")).toContain("skipping");
});
it("settles a phase an interrupted run left in_progress on a skipped check", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
// alpha finished, but a later interrupted run left its analysis phase open.
const state = initRunState(cwd, [
{ name: "alpha", label: "alpha", fix: false },
{ name: "beta", label: "beta", fix: false },
]);
state.recon = {
complete: true,
path: join(cwd, ".pygienium", "recon.json"),
finishedAt: Date.now(),
};
markComplete(state, "alpha");
// Directly, because applyPhaseStatus would also move the check itself back
// to in_progress — the corruption here is a phase left open behind a
// check that still reads complete.
const staleAnalysis = state.checks.alpha?.phases.find(
(p) => p.id === PHASE_ANALYSIS,
);
if (!staleAnalysis) throw new Error("alpha has no analysis phase");
staleAnalysis.status = "in_progress";
applyPhaseStatus(state, "beta", PHASE_ANALYSIS, "in_progress");
await saveRunState(state);
await captureStdout(() => handleAllCommand("", stubCtx(cwd)));
const after = await loadRunState(cwd);
// alpha was skipped as already complete: its stale phase settles to
// complete rather than leaving the check finished-but-running.
const alphaPhases = after?.checks.alpha?.phases ?? [];
expect(alphaPhases.every((p) => p.status !== "in_progress")).toBe(true);
expect(
alphaPhases.find((p) => p.id === PHASE_ANALYSIS)?.status,
).toBe("complete");
// beta never finished, so the run resumes it and its scan runs again
// (scan-only: this run passes no --fix).
expect(track.dispatched).toEqual(["beta"]);
});
it("resumes an interrupted run (re-dispatches non-terminal checks only)", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
// Seed an interrupted run: alpha complete, beta pending (interrupted).
const { initRunState, saveRunState } = await import("../src/run-state.js");
const state = initRunState(cwd, [
{ name: "alpha", label: "alpha", fix: true },
{ name: "beta", label: "beta", fix: true },
]);
state.recon = {
complete: true,
path: join(cwd, ".pygienium", "recon.json"),
finishedAt: Date.now(),
};
// Pre-create alpha's on-disk artifacts so its completed entry has artifacts.
const alphaDir = join(cwd, ".pygienium", "checks", "alpha");
await mkdir(alphaDir, { recursive: true });
await writeFile(
join(alphaDir, "findings.md"),
"# alpha findings\nalpha-scan\n",
);
await writeFile(
join(alphaDir, "changes.md"),
"# alpha changes\nalpha-fix\n",
);
markComplete(state, "alpha");
// beta was interrupted mid-analysis — left at in_progress.
applyPhaseStatus(state, "beta", PHASE_RECON, "complete");
applyPhaseStatus(state, "beta", PHASE_ANALYSIS, "in_progress");
await saveRunState(state);
// Resume via all-run: only beta should re-dispatch.
const out = await captureStdout(() =>
handleAllCommand("--fix", stubCtx(cwd)),
);
expect(track.dispatched).not.toContain("alpha");
expect(track.dispatched).toContain("beta");
expect(out.join("\n")).toContain("skipping");
const after = await loadRunState(cwd);
expect(after?.status).toBe("complete");
expect(after?.checks.alpha.status).toBe("complete");
expect(after?.checks.beta.status).toBe("complete");
});
it("runAllChecks supports scan-only (no fix phase, no changes artifacts)", async () => {
registerCheck(fakeCheck("alpha"));
const outcome = await runAllChecks({ cwd });
expect(outcome.ran).toEqual(["alpha"]);
expect(outcome.skipped).toEqual([]);
expect(outcome.status).toBe("complete");
const state = await loadRunState(cwd);
expect(state?.checks.alpha.fix).toBe(false);
expect(state?.checks.alpha.changes).toBeUndefined();
// Summary still written.
const summary = await readFile(outcome.summaryPath, "utf8");
expect(summary).toContain("## alpha — complete");
expect(outcome.summaryPath).toBe(allSummaryPath(cwd));
});
it("renders a summary even when no checks are registered/selected", async () => {
const outcome = await runAllChecks({ cwd, only: ["nonexistent"] });
expect(outcome.ran).toEqual([]);
const summary = await readFile(outcome.summaryPath, "utf8");
expect(summary).toContain("# Pygienium all-run summary");
expect(summary).toContain("- checks: 0");
});
it("renderAllSummary reflects per-check statuses and fix tags", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
await runAllChecks({ cwd, fix: true });
const state = (await loadRunState(cwd))!;
const md = renderAllSummary(state, selectChecks());
expect(md).toContain("## alpha — complete (--fix)");
expect(md).toContain("phases: recon:C");
});
it("all-summary never shows a stale error under a complete check", async () => {
const { initRunState } = await import("../src/run-state.js");
const state = initRunState(cwd, [
{ name: "alpha", label: "Alpha", fix: false },
]);
const alpha = fakeCheck("alpha");
// A hand-edited/legacy state can carry an error on a completed check
// (the MagnaFluo run showed exactly this shape).
markCheckStatus(state, "alpha", "complete", "legacy verify error");
const md = renderAllSummary(state, [alpha]);
expect(md).toContain("## alpha — complete");
expect(md).not.toContain("- error:");
// A genuinely failed check still surfaces its error.
markCheckStatus(state, "alpha", "failed", "scan exploded");
const md2 = renderAllSummary(state, [alpha]);
expect(md2).toContain("- error: scan exploded");
});
it("the unified strip surfaces every check name over the run", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const out = await captureStdout(() => handleAllCommand("", stubCtx(cwd)));
const joined = out.join("\n");
// Each check name appears in the strip line at least once.
expect(joined).toContain("Alpha");
expect(joined).toContain("Beta");
expect(joined).toContain("all [");
});
it("renders only the unified footer widget in UI mode (no per-check footer)", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const calls: Array<[string, string[] | undefined]> = [];
const ui = {
setWidget: (key: string, content: string[] | undefined) => {
calls.push([key, content]);
},
} as never; // stub ExtensionUIContext (tests have no pi type imports)
await runAllChecks({ cwd, ui, hasUI: true });
const keys = new Set(calls.map(([k]) => k));
// The all-run footer drives the belowEditor widget area under its own key…
expect(keys.has("pygienium-all")).toBe(true);
// …and per-check footers never claim their slot (footer:false), so two
// overviews never compete over the same widget area.
expect(keys.has("pygienium")).toBe(false);
// The widget is cleared when the run completes.
const last = calls[calls.length - 1];
expect(last).toBeDefined();
expect(last?.[0]).toBe("pygienium-all");
expect(last?.[1]).toBeUndefined();
});
});