import { test, expect, describe, beforeEach, afterEach, vi } from "vitest";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
/*
RFC-1190 acceptance tests: mission context propagation through
release-mode deployments — precedence chain, MISSION_CONTEXT_UNRESOLVED
blocking, evidence-sync ship steps, and journal record fields.
- 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 rfc suite's contract for test-file scaffold coverage.
*/
const executeKernelCommandMock = vi.fn<(opts: { commandName: string }) => Promise>();
vi.mock("@warpgogol/werkstatt-engine/kernel", () => ({
executeKernelCommand: (opts: { commandName: string }) => executeKernelCommandMock(opts),
KERNEL_UNIVERSAL_FLAGS: [],
}));
const readSystemStateMock = vi.fn<() => Promise>>();
vi.mock("../../sternsystem/registry-io.ts", () => ({
readSystemConfigSmart: vi.fn(async () => ({
deployment: {
adapter: "null",
channels: {
dev: { url: "https://dev.example" },
alt: { url: "https://alt.example" },
main: { url: "https://example.com" },
},
},
})),
readSystemStateSmart: vi.fn(async () => readSystemStateMock()),
resolveCacheClonePath: vi.fn((root: string, id: string) => path.join(root, "systems-cache", id)),
writeSystemStateSmart: vi.fn(async () => {}),
}));
vi.mock("../deploy-helpers.ts", async (importOriginal) => ({
...(await importOriginal()),
authorizeAndDeploy: vi.fn(async () => ({
ok: true,
outcome: { decisionId: "dec-1", ruleId: "ok", message: "ok" },
})),
buildEffectRecord: vi.fn(() => ({})),
writeDeploymentEffectRecord: vi.fn(async () => {}),
resolveArtifactHash: vi.fn(async () => ({ hash: "sha256:abc", source: "flag", path: undefined })),
resolveGateDecisionPath: vi.fn(
(_dir: string, _rel: string, _gate: string, flag?: string) => flag ?? "/tmp/gate.json",
),
}));
const executeDeployPhasesMock = vi.fn();
vi.mock("../deploy-execution.ts", async (importOriginal) => ({
...(await importOriginal()),
executeDeployPhases: (ctx: unknown, channel: string) => executeDeployPhasesMock(ctx, channel),
}));
const { runLeitstandDevDeploy, resolveMissionContext } = await import("../leitstand-commands.ts");
const { buildShipPlan } = await import("../ship.ts");
function makeContext(workspaceRoot: string) {
return {
io: createDefaultIO().io,
workspaceRoot,
logger: {
info: () => {},
success: () => {},
warn: () => {},
error: () => {},
debug: () => {},
},
} as never;
}
function makeInput(flags: Record) {
return { flags, argv: [] } as never;
}
async function writeBuildIdentity(
workspaceRoot: string,
releaseId: string,
identity: { missionId: string; commitSha: string },
): Promise {
const dir = path.join(workspaceRoot, "releases", releaseId, "dist", "client", ".well-known");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, "build-identity.json"), JSON.stringify(identity));
}
describe("RFC-1190 resolveMissionContext — precedence chain", () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc1190-"));
readSystemStateMock.mockReset();
readSystemStateMock.mockResolvedValue({});
executeDeployPhasesMock.mockReset();
executeKernelCommandMock.mockReset();
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("AC-2: no flag + null currentMission resolves from release build-identity.json", async () => {
await writeBuildIdentity(tmpDir, "acme-r000001", {
missionId: "acme-m000042",
commitSha: "deadbeef",
});
const ctx = await resolveMissionContext(tmpDir, "acme", createDefaultIO().io, {
releaseId: "acme-r000001",
});
expect(ctx).toEqual({
missionId: "acme-m000042",
commitSha: "deadbeef",
source: "build-identity",
});
});
test("explicit --mission wins over build-identity when they agree", async () => {
await writeBuildIdentity(tmpDir, "acme-r000001", {
missionId: "acme-m000042",
commitSha: "deadbeef",
});
const ctx = await resolveMissionContext(tmpDir, "acme", createDefaultIO().io, {
missionFlag: "acme-m000042",
releaseId: "acme-r000001",
});
expect(ctx.source).toBe("flag");
expect(ctx.missionId).toBe("acme-m000042");
expect(ctx.commitSha).toBe("deadbeef");
});
test("explicit --mission contradicting build-identity throws MISSION_CONTEXT_MISMATCH", async () => {
await writeBuildIdentity(tmpDir, "acme-r000001", {
missionId: "acme-m000042",
commitSha: "deadbeef",
});
await expect(
resolveMissionContext(tmpDir, "acme", createDefaultIO().io, {
missionFlag: "acme-m000099",
releaseId: "acme-r000001",
}),
).rejects.toThrow("MISSION_CONTEXT_MISMATCH");
});
test("falls back to system-state.currentMission when no flag and no build-identity", async () => {
readSystemStateMock.mockResolvedValue({ currentMission: "acme-m000007" });
const ctx = await resolveMissionContext(tmpDir, "acme", createDefaultIO().io, {
releaseId: "acme-r000001",
});
expect(ctx.source).toBe("current-mission");
expect(ctx.missionId).toBe("acme-m000007");
});
test("returns source 'none' when nothing resolves", async () => {
const ctx = await resolveMissionContext(tmpDir, "acme", createDefaultIO().io, {
releaseId: "acme-r000001",
});
expect(ctx).toEqual({ missionId: "", commitSha: "", source: "none" });
});
});
describe("RFC-1190 leitstand.dev-deploy — unresolved mission context", () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc1190-deploy-"));
readSystemStateMock.mockReset();
readSystemStateMock.mockResolvedValue({}); // currentMission: null
executeDeployPhasesMock.mockReset();
executeDeployPhasesMock.mockResolvedValue({
deploymentUrl: "https://dev.example",
failingPhase: undefined,
buildSkipped: true,
buildIdentity: { releaseId: "acme-r000001", written: false, path: "" },
axiomStatus: "pass",
axiomExitCode: 0,
freshness: { verified: true, cdnDistTreeHash: "h", localDistTreeHash: "h", attempts: 1 },
evidenceSynced: false,
evidenceSyncError: null,
});
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
test("AC-3: exits non-zero with MISSION_CONTEXT_UNRESOLVED when nothing resolves", async () => {
const result = (await runLeitstandDevDeploy(
makeInput({ site: "acme", release: "acme-r000001" }),
makeContext(tmpDir),
)) as { exitCode: number; diagnostics?: Array<{ ruleId: string }>; summary: string };
expect(result.exitCode).toBe(1);
expect(result.diagnostics?.[0]?.ruleId).toBe("MISSION_CONTEXT_UNRESOLVED");
expect(result.summary).toContain("MISSION_CONTEXT_UNRESOLVED");
// The deploy pipeline must NOT have been reached
expect(executeDeployPhasesMock).not.toHaveBeenCalled();
});
test("AC-3: --skip-evidence-sync is the explicit escape hatch — deploy proceeds", async () => {
const result = (await runLeitstandDevDeploy(
makeInput({ site: "acme", release: "acme-r000001", "skip-evidence-sync": true }),
makeContext(tmpDir),
)) as { exitCode: number };
expect(result.exitCode).toBe(0);
expect(executeDeployPhasesMock).toHaveBeenCalledOnce();
const ctxArg = executeDeployPhasesMock.mock.calls[0]![0] as {
skipEvidenceSync: boolean;
missionId?: string;
};
expect(ctxArg.skipEvidenceSync).toBe(true);
expect(ctxArg.missionId).toBe("");
});
test("resolved context flows into executeDeployPhases (missionId + commitSha + record)", async () => {
await writeBuildIdentity(tmpDir, "acme-r000001", {
missionId: "acme-m000042",
commitSha: "cafebabe",
});
const result = (await runLeitstandDevDeploy(
makeInput({ site: "acme", release: "acme-r000001" }),
makeContext(tmpDir),
)) as { exitCode: number; data: { missionId: string; axiom: { status: string } } };
expect(result.exitCode).toBe(0);
const ctxArg = executeDeployPhasesMock.mock.calls[0]![0] as {
missionId: string;
commitSha: string;
missionContext: { source: string };
};
expect(ctxArg.missionId).toBe("acme-m000042");
expect(ctxArg.commitSha).toBe("cafebabe");
expect(ctxArg.missionContext.source).toBe("build-identity");
// AC-1: the step journal record (built from deploy result data) carries
// missionId + a real axiom status — not "not-run".
expect(result.data.missionId).toBe("acme-m000042");
expect(result.data.axiom.status).toBe("pass");
});
});
describe("RFC-1190 buildShipPlan — evidence-sync steps", () => {
test("AC-5: evidence-sync-* step follows each certify-* step", () => {
const plan = buildShipPlan({ until: "archived" });
const names = plan.steps.map((s) => s.name);
expect(names.indexOf("evidence-sync-dev")).toBe(names.indexOf("certify-dev") + 1);
expect(names.indexOf("evidence-sync-alt")).toBe(names.indexOf("certify-alt") + 1);
expect(names.indexOf("evidence-sync-main")).toBe(names.indexOf("certify-main") + 1);
});
test("AC-5: evidence-sync steps survive the closed-mission plan shape", () => {
const plan = buildShipPlan({ until: "archived", missionClosed: true });
const names = plan.steps.map((s) => s.name);
expect(names).toContain("evidence-sync-dev");
expect(names).toContain("evidence-sync-alt");
expect(names).toContain("evidence-sync-main");
expect(names).not.toContain("mission-validate");
expect(names).not.toContain("mission-close");
});
test("evidence-sync step is a clean no-op when evidence/axiom is absent", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc1190-ship-"));
executeKernelCommandMock.mockReset();
try {
const missionDir = path.join(tmpDir, "missions", "acme-m000042");
await fs.mkdir(missionDir, { recursive: true });
const plan = buildShipPlan({ until: "archived" });
const step = plan.steps.find((s) => s.name === "evidence-sync-dev")!;
const ctx = {
workspaceRoot: tmpDir,
systemId: "acme",
missionId: "acme-m000042",
missionDir,
workpieceDir: path.join(missionDir, "workpiece"),
cacheCloneDir: path.join(tmpDir, "systems-cache", "acme"),
releaseId: "acme-r000001",
missionClosed: false,
logger: { info: () => {} },
io: createDefaultIO().io,
};
const meta = (await step.run(ctx as never)) as { skipped: boolean; missionId: string };
expect(meta.skipped).toBe(true);
expect(meta.missionId).toBe("acme-m000042");
expect(executeKernelCommandMock).not.toHaveBeenCalled();
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
test("AC-4: evidence.sync failure while evidence exists throws EVIDENCE_SYNC_FAILED", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc1190-ship-"));
executeKernelCommandMock.mockReset();
try {
const evidenceDir = path.join(tmpDir, "missions", "acme-m000042", "evidence", "axiom");
await fs.mkdir(evidenceDir, { recursive: true });
await fs.writeFile(
path.join(evidenceDir, "evidence-metadata.json"),
JSON.stringify({ runTimestamp: "2026-10-04T00-00-00-000Z" }),
);
executeKernelCommandMock.mockResolvedValue({ exitCode: 1, summary: "R2 upload failed" });
const plan = buildShipPlan({ until: "archived" });
const step = plan.steps.find((s) => s.name === "evidence-sync-dev")!;
const ctx = {
workspaceRoot: tmpDir,
systemId: "acme",
missionId: "acme-m000042",
missionDir: path.join(tmpDir, "missions", "acme-m000042"),
workpieceDir: path.join(tmpDir, "missions", "acme-m000042", "workpiece"),
cacheCloneDir: path.join(tmpDir, "systems-cache", "acme"),
releaseId: "acme-r000001",
missionClosed: false,
logger: { info: () => {} },
io: createDefaultIO().io,
};
await expect(step.run(ctx as never)).rejects.toThrow("EVIDENCE_SYNC_FAILED");
// AC-5: invoked with --site and --mission
const syncCall = executeKernelCommandMock.mock.calls.find(
(c) => c[0].commandName === "evidence.sync",
);
expect(syncCall).toBeDefined();
expect(syncCall![0]).toMatchObject({
argv: expect.arrayContaining(["--site=acme", "--mission=acme-m000042"]),
});
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
});