/*
RFC-1263 AC-5: regression pin — leitstand.ship still runs the full
mission.validate before any lifecycle/deploy phase; the content fast lane
relaxes only the pre-ship reconcile/close precondition, never ship.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1263: initial pin — validate precedes reconcile/close in ship.
- RFC-1263
- RFC-1326: single Cloudflare permission registry, token preflight, birth provisioning
*/
import { test, expect, vi, beforeEach, afterEach } from "vitest";
import { runLeitstandShip } from "./ship.ts";
import { executeKernelCommand } from "@warpgogol/werkstatt-engine/kernel";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import type { KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, executeKernelCommand: vi.fn() };
});
vi.mock("../kernel/runtime/registry-cache.ts", () => ({
getOrBuildWorkspaceRegistry: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@warpgogol/werkstatt-site/checks", () => ({
ensureChromium: vi.fn().mockResolvedValue({ ok: true }),
}));
vi.mock("../sternsystem/registry-io.ts", () => ({
resolveCacheClonePath: vi.fn((workspaceRoot: string, systemId: string) =>
path.join(workspaceRoot, "systems-cache", systemId),
),
}));
const mockExecuteKernelCommand = executeKernelCommand as unknown as ReturnType;
const mockIoExec = vi.fn();
const context = {
io: { ...createDefaultIO().io, exec: mockIoExec },
workspaceRoot: "/tmp/test-ship-phases",
logger: { info: () => {}, success: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
} as unknown as KernelRuntimeContext;
let tmpDir: string;
beforeEach(async () => {
mockExecuteKernelCommand.mockReset();
mockIoExec.mockReset();
mockIoExec.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "ship-phases-"));
await fs.mkdir(path.join(tmpDir, "systems-cache", "test-sys", "operations"), {
recursive: true,
});
const envPath = path.join(tmpDir, "missions", "m000001", "workpiece", ".env");
await fs.mkdir(path.dirname(envPath), { recursive: true });
await fs.writeFile(envPath, "TEST=1\n");
(context as unknown as { workspaceRoot: string }).workspaceRoot = tmpDir;
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("AC-5: ship runs mission.validate before reconcile/close — a fast-lane record never substitutes", async () => {
for (const name of [
"leitstand.status",
"cloudflare.token.check",
"agent.search.index.create",
"agent.search.warm",
"mission.validate",
"mission.reconcile",
"mission.close",
]) {
mockExecuteKernelCommand.mockImplementationOnce(async () => ({
exitCode: 0,
data: {},
summary: `[${name}] ok`,
}));
}
const result = await runLeitstandShip(
{ flags: { site: "test-sys", mission: "m000001", until: "closed" }, argv: [] },
context,
);
expect(result.exitCode).toBe(0);
const order = mockExecuteKernelCommand.mock.calls.map(
(c) => (c[0] as { commandName: string }).commandName,
);
expect(order).toContain("mission.validate");
expect(order.indexOf("mission.validate")).toBeLessThan(order.indexOf("mission.reconcile"));
expect(order.indexOf("mission.validate")).toBeLessThan(order.indexOf("mission.close"));
});