initial import: @mikefreno/omp-pygenium (omp port)

This commit is contained in:
2026-08-10 09:46:09 -04:00
commit a40cdcd9e3
70 changed files with 12624 additions and 0 deletions

View File

@@ -0,0 +1,422 @@
/**
* status-resume-export.test.ts — integration test for task 13.
*
* Mirrors the spec scenario: start a hypothetical `/pygienium-all`, treat it as
* interrupted (one check complete, one pending), then exercise
* `/pygienium-status`, `/pygienium-resume`, and `/pygienium-export` and assert:
*
* - status reports accurate per-check progress (one complete, one pending,
* run still in_progress);
* - resume re-dispatches the pending check and DOES NOT re-run the complete
* one (proven via the agent-runner call log), and the run ends complete;
* - --fresh re-dispatches even the complete check (proven via call log);
* - export produces a filtered markdown bundle on disk, and the --check=
* and --out=json filters work.
*
* A tracker wraps the fake agent runner so we can assert which checks were
* actually dispatched without depending on timing or a model.
*/
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, relative } 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 {
handleStatusCommand,
handleResumeCommand,
handleExportCommand,
type PygieniumCtx,
} from "../src/commands.js";
import {
initRunState,
loadRunState,
saveRunState,
markCheckStatus,
recordCheckOutput,
applyPhaseStatus,
PHASE_RECON,
PHASE_ANALYSIS,
PHASE_FIX,
PHASE_VERIFY,
PHASE_CLEANUP,
} from "../src/run-state.js";
import { formatRunStatus } from "../src/status.js";
import {
exportRun,
gatherExportEntries,
renderExportJson,
renderExportMarkdown,
parseExportFilters,
canonicalChecksRoot,
} from "../src/export.js";
import type { AgentTaskOptions } from "../src/agent-runner.js";
/** Build a deterministic check whose fake runner writes on-disk artifacts. */
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, 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) => {
// Tag by check name from the task text (`!write .pygienium/checks/<name>/`).
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 with captured output. */
function markComplete(
state: Parameters<typeof markCheckStatus>[0],
name: string,
findings: string,
changes: string,
): void {
for (const phaseId of [
PHASE_RECON,
PHASE_ANALYSIS,
PHASE_FIX,
PHASE_VERIFY,
PHASE_CLEANUP,
]) {
applyPhaseStatus(state, name, phaseId, "complete");
}
recordCheckOutput(state, name, { findings, changes });
markCheckStatus(state, name, "complete");
}
describe("status / resume / export (task 13)", () => {
let cwd: string;
let track: ReturnType<typeof trackingRunner>;
beforeEach(async () => {
clearChecks();
track = trackingRunner();
setAgentRunner(track.runner);
cwd = await mkdtemp(join(tmpdir(), "pygienium-resume-"));
});
afterEach(async () => {
resetAgentRunner();
await rm(cwd, { recursive: true, force: true });
});
/** Seed an interrupted two-check run: `alpha` complete, `beta` pending. */
async function seedInterruptedRun(
fix = true,
): Promise<{ state: ReturnType<typeof initRunState> }> {
const state = initRunState(cwd, [
{ name: "alpha", label: "alpha", fix },
{ name: "beta", label: "beta", fix },
]);
state.recon = {
complete: true,
path: `${cwd}/.pygienium/recon.json`,
finishedAt: Date.now(),
};
// Simulate alpha fully complete with on-disk artifacts + captured text.
markComplete(
state,
"alpha",
"# alpha findings\nalpha-scan",
"# alpha changes\nalpha-fix",
);
const alphaDir = join(canonicalChecksRoot(cwd), "alpha");
await mkdir(alphaDir, { recursive: true });
await writeFile(
join(alphaDir, "findings.md"),
"# alpha findings\nalpha-scan\n",
"utf8",
);
await writeFile(
join(alphaDir, "changes.md"),
"# alpha changes\nalpha-fix\n",
"utf8",
);
// beta left pending (the interruption). For alpha's recon phase, mark it too.
applyPhaseStatus(state, "beta", PHASE_RECON, "complete");
await saveRunState(state);
return { state };
}
it("formatRunStatus reports accurate per-check progress (alpha complete, beta pending)", async () => {
const { state } = await seedInterruptedRun();
const lines = formatRunStatus(state);
expect(lines.join("\n")).toContain("pygienium run — in_progress");
expect(lines.join("\n")).toContain("alpha — complete");
expect(lines.join("\n")).toContain("beta — pending");
// Artifacts captured on alpha appear; beta has none.
expect(lines.join("\n")).toContain("findings: 2 line(s)");
expect(lines.join("\n")).toContain("changes: 2 line(s)");
});
it("/pygienium-status prints the status line list end to end", async () => {
const { state } = await seedInterruptedRun();
const out = await captureStdout(() =>
handleStatusCommand("", stubCtx(cwd)),
);
expect(out.length).toBeGreaterThan(0);
expect(out.join("\n")).toContain("alpha — complete");
expect(out.join("\n")).toContain("beta — pending");
expect(out.join("\n")).toContain("pygienium run — in_progress");
void state;
});
it("/pygienium-status with no run state prints a not-found message", async () => {
const out = await captureStdout(() =>
handleStatusCommand("", stubCtx(cwd)),
);
expect(out.join("\n")).toContain("no run state found");
});
it("/pygienium-resume re-dispatches beta without re-running complete alpha", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const { state } = await seedInterruptedRun();
void state;
const out = await captureStdout(() =>
handleResumeCommand("", stubCtx(cwd)),
);
// alpha is complete and must NOT be re-dispatched; beta was pending.
expect(track.dispatched).not.toContain("alpha");
expect(track.dispatched).toContain("beta");
// The run should now be complete.
const after = await loadRunState(cwd);
expect(after?.status).toBe("complete");
expect(after?.checks.alpha.status).toBe("complete");
expect(after?.checks.beta.status).toBe("complete");
// beta's artifacts now exist on disk.
const betaFindings = await readFile(
join(canonicalChecksRoot(cwd), "beta", "findings.md"),
"utf8",
);
expect(betaFindings).toContain("beta findings");
// Summary mentions re-dispatched/skipped counts.
expect(out.join("\n")).toContain("re-dispatched 1");
expect(out.join("\n")).toContain("skipped 1");
});
it("/pygienium-resume --fresh re-dispatches the complete check too", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
await seedInterruptedRun();
await captureStdout(() => handleResumeCommand("--fresh", stubCtx(cwd)));
expect(track.dispatched).toContain("alpha");
expect(track.dispatched).toContain("beta");
const after = await loadRunState(cwd);
expect(after?.status).toBe("complete");
});
it("/pygienium-resume with no state prints nothing-to-resume", async () => {
const out = await captureStdout(() =>
handleResumeCommand("", stubCtx(cwd)),
);
expect(out.join("\n")).toContain("no run state to resume");
});
it("/pygienium-resume on an already-complete run refuses without --fresh", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const { state } = await seedInterruptedRun();
// Complete beta too so the whole run is complete.
markComplete(
state,
"beta",
"# beta findings\nbeta-scan",
"# beta changes\nbeta-fix",
);
await saveRunState(state);
const out = await captureStdout(() =>
handleResumeCommand("", stubCtx(cwd)),
);
expect(out.join("\n")).toContain("nothing to resume");
expect(track.dispatched).toHaveLength(0);
});
it("/pygienium-export writes a markdown bundle with both checks", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const { state } = await seedInterruptedRun();
// Run beta via resume so its artifacts land on disk.
await captureStdout(() => handleResumeCommand("", stubCtx(cwd)));
void state;
await captureStdout(() => handleExportCommand("", stubCtx(cwd)));
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
expect(bundle).toContain("# Pygienium export");
expect(bundle).toContain("## alpha (complete)");
expect(bundle).toContain("## beta (complete)");
expect(bundle).toContain("# alpha findings");
expect(bundle).toContain("# beta findings");
});
it("/pygienium-export --check=beta produces a filtered bundle", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
await seedInterruptedRun();
await captureStdout(() => handleResumeCommand("", stubCtx(cwd)));
await captureStdout(() =>
handleExportCommand("--check=beta", stubCtx(cwd)),
);
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
expect(bundle).toContain("## beta (complete)");
expect(bundle).not.toContain("## alpha");
});
it("/pygienium-export --status=failed includes only failed checks", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
const { state } = await seedInterruptedRun();
// Mark alpha failed (artifacts already on disk from the seed); leave beta pending.
markCheckStatus(state, "alpha", "failed", "fake failure");
await saveRunState(state);
await captureStdout(() =>
handleExportCommand("--status=failed", stubCtx(cwd)),
);
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
expect(bundle).toContain("## alpha (failed)");
expect(bundle).not.toContain("## beta");
});
it("/pygienium-export --out=json writes JSON matching renderExportJson", async () => {
registerCheck(fakeCheck("alpha"));
registerCheck(fakeCheck("beta"));
await seedInterruptedRun();
await captureStdout(() => handleResumeCommand("", stubCtx(cwd)));
const out = await captureStdout(() =>
handleExportCommand("--out=json", stubCtx(cwd)),
);
expect(out.join("\n")).toContain("export.json");
const raw = await readFile(join(cwd, ".pygienium", "export.json"), "utf8");
const parsed = JSON.parse(raw) as {
checks: Array<{ name: string; status: string; findings: string }>;
};
const names = parsed.checks.map((c) => c.name).sort();
expect(names).toEqual(["alpha", "beta"]);
expect(parsed.checks.find((c) => c.name === "alpha")?.findings).toContain(
"alpha findings",
);
// renderExportJson matches the on-disk content for the gathered set.
const state = await loadRunState(cwd);
const entries = await gatherExportEntries(cwd, state);
expect(renderExportJson(state, entries).trim()).toBe(raw.trim());
});
it("parseExportFilters splits comma lists and trims values", () => {
const f = parseExportFilters(
"--check=alpha,beta --status=complete,failed --out=json",
);
expect(f.check).toEqual(["alpha", "beta"]);
expect(f.status).toEqual(["complete", "failed"]);
expect(f.out).toBe("json");
});
it("gatherExportEntries reads only the canonical .pygienium/checks/ root", async () => {
await mkdir(join(cwd, ".pygienium", "checks", "alpha"), {
recursive: true,
});
await writeFile(
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
"# alpha findings\n",
"utf8",
);
// A stray pygienium/checks/ dir (the old non-hidden root) is ignored now
// that all checks write to the single canonical `.pygienium/checks/` root.
await mkdir(join(cwd, "pygienium", "checks", "ghost"), {
recursive: true,
});
await writeFile(
join(cwd, "pygienium", "checks", "ghost", "findings.md"),
"# ghost findings\n",
"utf8",
);
const entries = await gatherExportEntries(cwd, undefined);
const alpha = entries.find((e) => e.name === "alpha");
expect(alpha).toBeDefined();
expect(alpha?.findings).toContain("alpha findings");
expect(alpha?.status).toBe("unknown");
expect(alpha?.findingsPath).toBe(
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
);
expect(entries.find((e) => e.name === "ghost")).toBeUndefined();
});
it("renderExportMarkdown includes a 'no artifacts' note for empty checks", async () => {
const entries = [{ name: "ghost", status: "unknown" }];
const md = renderExportMarkdown(undefined, entries as never);
expect(md).toContain("## ghost (unknown)");
expect(md).toContain("no findings.md or changes.md on disk");
// And renderExportJson emits nulls for the empty check.
const json = renderExportJson(undefined, entries as never);
const parsed = JSON.parse(json) as {
checks: Array<{ findings: unknown; changes: unknown }>;
};
expect(parsed.checks[0]!.findings).toBeNull();
expect(parsed.checks[0]!.changes).toBeNull();
});
it("exportRun writes nothing useful and reports zero entries cleanly", async () => {
const result = await exportRun(cwd, undefined, {});
expect(result.entries).toHaveLength(0);
expect(relative(cwd, result.path)).toBe(join(".pygienium", "export.md"));
});
});