/** * * Shared run-twice harness for `rerun-safe` command tests (RFC-1327): runs a command closure twice against one fixture, snapshots caller-owned state between runs, and asserts the second run converges — same exit status, no unexpected tree/git diff. Remote effects are exercised through the `context.io.exec` seam the command already uses (stub it in `run`), never mocked around it. * expectRerunSafe runner; snapshotGitTree probe (HEAD + porcelain status); SnapshotDiff report shape * vitest expect; node:child_process execSync for git probes * none — delegates all mutation to the caller's run() closure * assertion failure reports the diff list; run() exit-code shape is caller-defined via RunResult * kernel dispatch (tests import handlers directly); remote-state stubbing (callers stub context.io.exec); multi-run (>2) sequences * * * - Exports: expectRerunSafe, snapshotGitTree, RerunCase, RunResult. * - Contract: second run MUST report the same outcome as a converged-state run (exit 0 / same status) and MUST NOT change the snapshotted state — convergence, not absence of work. * * * * * * * * RFC-1327: caller supplies run+snapshot so the harness stays handler-agnostic — kernel commands in this repo are invoked as imported functions in tests, not via a dispatch table. * Snapshots are opaque strings compared by equality — domain-specific compare (e.g. ignoring timestamps) is the caller's normalizer, keeping the harness free of per-command special cases. * * RFC-1327 * * RFC-1327: initial harness — expectRerunSafe + snapshotGitTree per the accepted plan. * */ import { execSync } from "node:child_process"; import { expect } from "vitest"; /** What a single run reports back to the harness. */ export interface RunResult { /** Process-style exit code; omit when the run has no exit-code surface. */ exitCode?: number; /** Semantic status (e.g. "pass", "skipped", "already-reconciled"). */ status?: string; } export interface RerunCase { /** Execute the command under test once (run 1 and run 2 share the fixture). */ run: () => Promise; /** Fingerprint the state the command owns — must be deterministic. */ snapshot: () => Promise | TSnapshot; /** Optional second-run expectation override; defaults to equal status + equal snapshot. */ expectSecondRun?: (first: TRun, second: TRun) => void; } /** * Run `probe.run()` twice and assert the second run converges: * identical `status`/`exitCode` and an unchanged snapshot. */ export async function expectRerunSafe( probe: RerunCase, ): Promise<{ first: TRun; second: TRun; snapshot: TSnapshot; }> { const first = await probe.run(); const before = await probe.snapshot(); const second = await probe.run(); const after = await probe.snapshot(); if (probe.expectSecondRun) { probe.expectSecondRun(first, second); } else { expect(second.status).toBe(first.status); expect(second.exitCode).toBe(first.exitCode); } expect(after, "second run changed owned state — command is not rerun-safe").toEqual(before); return { first, second, snapshot: after }; } /** * Git-state probe for rerun tests: `\n` of `dir`. * Captures commit movement plus dirty/untracked drift in one string. */ export function snapshotGitTree(dir: string): string { const git = (args: string) => execSync(`git ${args}`, { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }).trim(); return `${git("rev-parse HEAD")}\n${git("status --porcelain")}`; }