/*
Unit tests for runner property covering its documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the runner suite's contract for test-file scaffold coverage.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import fc from "fast-check";
import { runOperation, appendRecord } from "../runner.ts";
import type { OperationDefinition } from "../types.ts";
interface Ctx {
effects: Set;
}
function makeCtx(): Ctx {
return { effects: new Set() };
}
function makeStep(name: string) {
return {
name,
async run(c: Ctx) {
c.effects.add(name);
},
verify: async (c: Ctx) => c.effects.has(name),
};
}
function makeSteps(n: number): { steps: ReturnType[]; ctx: Ctx } {
const ctx = makeCtx();
const steps = [];
for (let i = 0; i < n; i++) {
steps.push(makeStep(`s${i}`));
}
return { steps, ctx };
}
async function simulateCrashAtSeq(
journalPath: string,
opId: string,
op: string,
crashSeq: number,
numSteps: number,
): Promise {
await appendRecord(journalPath, {
kind: "op-started",
opId,
op,
missionId: "m-pbt",
at: new Date().toISOString(),
platformVersion: "1.0.0",
});
for (let i = 0; i < crashSeq; i++) {
await appendRecord(journalPath, {
kind: "step-started",
opId,
step: `s${i}`,
seq: i,
at: new Date().toISOString(),
});
await appendRecord(journalPath, {
kind: "step-done",
opId,
step: `s${i}`,
seq: i,
at: new Date().toISOString(),
});
}
if (crashSeq < numSteps) {
await appendRecord(journalPath, {
kind: "step-started",
opId,
step: `s${crashSeq}`,
seq: crashSeq,
at: new Date().toISOString(),
});
}
}
let tmpDir: string;
let journalPath: string;
beforeEach(() => {
tmpDir = mkdtempSync(path.join(os.tmpdir(), "journal-pbt-"));
journalPath = path.join(tmpDir, "journal.jsonl");
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
test("crash at every seq: resume completes with zero manual intervention", async () => {
await fc.assert(
fc.asyncProperty(fc.integer({ min: 1, max: 6 }), async (numSteps) => {
for (let crashSeq = 0; crashSeq <= numSteps; crashSeq++) {
rmSync(tmpDir, { recursive: true, force: true });
const { steps: stepDefs, ctx } = makeSteps(numSteps);
const def: OperationDefinition = { op: "test.pbt", steps: stepDefs };
const opId = `pbt-crash-${numSteps}-${crashSeq}`;
await simulateCrashAtSeq(journalPath, opId, "test.pbt", crashSeq, numSteps);
const resumeResult = await runOperation(journalPath, def, ctx, { resumeOpId: opId });
expect(
resumeResult.completed,
`crash at seq ${crashSeq} of ${numSteps} did not resume to completion`,
).toBe(true);
const expectedExecuted = numSteps - crashSeq;
expect(
ctx.effects.size,
`expected ${expectedExecuted} steps executed after resume from crash at seq ${crashSeq}`,
).toBe(expectedExecuted);
}
}),
{ numRuns: 5 },
);
});
test("crash at random seq: resume completes all effects", async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 2, max: 8 }),
fc.integer({ min: 0, max: 8 }),
async (numSteps, crashSeq) => {
const actualCrash = Math.min(crashSeq, numSteps);
rmSync(tmpDir, { recursive: true, force: true });
const { steps: stepDefs, ctx } = makeSteps(numSteps);
const def: OperationDefinition = { op: "test.pbt2", steps: stepDefs };
const opId = `pbt-rand-${numSteps}-${actualCrash}`;
await simulateCrashAtSeq(journalPath, opId, "test.pbt2", actualCrash, numSteps);
const resumeResult = await runOperation(journalPath, def, ctx, { resumeOpId: opId });
expect(resumeResult.completed, `crash at seq ${actualCrash} of ${numSteps}`).toBe(true);
const expectedExecuted = numSteps - actualCrash;
expect(ctx.effects.size).toBe(expectedExecuted);
},
),
{ numRuns: 10 },
);
});