/*
RFC-1327 AC-5: leitstand.ship mission-reconcile step skip — when the mission's
reconciliation-report.json already records the current workpiece head, the
step returns skipped:true without invoking mission.reconcile; a head
mismatch runs the command normally.
- Not a full ship-plan execution — the reconcile step is exercised in isolation over tmp fixtures.
- RFC-1327: initial rerun coverage for the ship reconcile-skip gate.
- RFC-1327: run-twice idempotency — declarations, command.idempotency.validate, rerun tests, reconcile/ship/abort convergence fixes
- RFC-1327: review r1 — lock-safe reconcile gate, residue sweep, honest idempotency classes
- RFC-1327: explicit test timeouts on rerun tests — root vitest testTimeout is 5s
*/
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import type { ShipContext } from "../ship.ts";
const executeKernelCommandMock = vi.fn<(opts: { commandName: string }) => Promise>();
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
executeKernelCommand: (opts: { commandName: string }) => executeKernelCommandMock(opts),
};
});
const { buildShipPlan } = await import("../ship.ts");
function gitInit(dir: string): string {
execSync("git init -b main", { cwd: dir, stdio: "pipe" });
execSync("git config user.email test@test.com", { cwd: dir, stdio: "pipe" });
execSync("git config user.name Test", { cwd: dir, stdio: "pipe" });
execSync("git commit -qm init --allow-empty", { cwd: dir, stdio: "pipe" });
return execSync("git rev-parse HEAD", { cwd: dir, encoding: "utf8" }).trim();
}
function makeCtx(root: string, missionId: string): ShipContext {
return {
workspaceRoot: root,
systemId: "acme",
missionId,
missionDir: path.join(root, "missions", missionId),
workpieceDir: path.join(root, "missions", missionId, "workpiece"),
cacheCloneDir: path.join(root, "systems-cache", "acme"),
releaseId: null,
missionClosed: false,
allowUnreconciled: false,
logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {} } as never,
io: createDefaultIO().io,
};
}
async function writeReport(ctx: ShipContext, head: string | null): Promise {
const dir = path.join(ctx.missionDir, "evidence");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(
path.join(dir, "reconciliation-report.json"),
JSON.stringify({ workpieceHeadAtReconcile: head }) + "\n",
);
}
describe("leitstand.ship mission-reconcile skip (RFC-1327)", () => {
let root: string;
beforeEach(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), "ship-rerun-"));
executeKernelCommandMock.mockReset();
executeKernelCommandMock.mockResolvedValue({ exitCode: 0, summary: "ok", data: {} });
});
afterEach(async () => {
await fs.rm(root, { recursive: true, force: true });
});
test("skips reconcile when the report already folded the current head", { timeout: 30_000 }, async () => {
const ctx = makeCtx(root, "acme-m000001");
await fs.mkdir(ctx.workpieceDir, { recursive: true });
const head = gitInit(ctx.workpieceDir);
await writeReport(ctx, head);
const plan = buildShipPlan({ until: "closed" });
const step = plan.steps.find((s) => s.name === "mission-reconcile");
expect(step).toBeDefined();
const result = (await step!.run(ctx)) as { skipped?: boolean };
expect(result.skipped).toBe(true);
expect(
executeKernelCommandMock.mock.calls.some(
(c) => c[0].commandName === "mission.reconcile",
),
"mission.reconcile must not be invoked on an already-reconciled head",
).toBe(false);
});
test("runs reconcile when the recorded head is stale", { timeout: 30_000 }, async () => {
const ctx = makeCtx(root, "acme-m000002");
await fs.mkdir(ctx.workpieceDir, { recursive: true });
gitInit(ctx.workpieceDir);
await writeReport(ctx, "0".repeat(40));
const plan = buildShipPlan({ until: "closed" });
const step = plan.steps.find((s) => s.name === "mission-reconcile")!;
const result = (await step.run(ctx)) as { skipped?: boolean };
expect(result.skipped).toBe(false);
expect(
executeKernelCommandMock.mock.calls.some((c) => c[0].commandName === "mission.reconcile"),
).toBe(true);
});
test("runs reconcile when no report exists", { timeout: 30_000 }, async () => {
const ctx = makeCtx(root, "acme-m000003");
await fs.mkdir(ctx.workpieceDir, { recursive: true });
gitInit(ctx.workpieceDir);
const plan = buildShipPlan({ until: "closed" });
const step = plan.steps.find((s) => s.name === "mission-reconcile")!;
await step.run(ctx);
expect(
executeKernelCommandMock.mock.calls.some((c) => c[0].commandName === "mission.reconcile"),
).toBe(true);
});
});