/*
RFC-1264 AC-1/AC-6: mission.close runs the mission-digest step pre-bordbuch,
links the artifact path into the close bordbuch event, and surfaces it in the
close report; --skip-digest is the audited escape hatch.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1264: initial test file.
- RFC-1264
*/
import { test, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { tmpdir } from "node:os";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { runMissionClose } from "./mission-close.ts";
// ─── mocks ───────────────────────────────────────────────────────────────────
const mockExecuteKernelCommand = vi.hoisted(() => vi.fn());
const mockRunMissionValidate = vi.hoisted(() =>
vi.fn(async () => ({
exitCode: 0,
data: { contractFull: { passed: true, validators: [] } },
summary: "Validation passed",
})),
);
const mockAppendBordbuch = vi.hoisted(() =>
vi.fn(async () => ({
entry: { id: "event-000001", kind: "mission-close" },
commitResult: { commitSha: "abc123", pushed: true, error: null },
})),
);
vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, executeKernelCommand: mockExecuteKernelCommand };
});
vi.mock("../mission/mission-materialization-commands.ts", async (importOriginal) => {
const actual =
await importOriginal();
return { ...actual, runMissionValidate: mockRunMissionValidate };
});
vi.mock("../bordbuch/bordbuch-io.ts", () => ({
validateBordbuch: vi.fn(async () => ({ entries: 0, violations: [] })),
readBordbuch: vi.fn(async () => []),
commitAndPushBordbuch: vi.fn(async () => ({ commitSha: "abc123", pushed: true, error: null })),
}));
vi.mock("../bordbuch/bordbuch-commit-helper.ts", () => ({
appendAndCommitBordbuch: mockAppendBordbuch,
}));
// ─── fixture ─────────────────────────────────────────────────────────────────
const SITE = "test-sys";
const MISSION = `${SITE}-m000001`;
const DIGEST_PATH = `missions/${MISSION}/evidence/mission-digest/digest.md`;
let testRoot: string;
let workspaceRoot: string;
let cacheClone: string;
let workpiece: string;
function write(base: string, rel: string, content: string): void {
const abs = join(base, rel);
mkdirSync(join(abs, ".."), { recursive: true });
writeFileSync(abs, content);
}
function gitInit(dir: string): void {
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" });
}
function gitCommitAll(dir: string, msg: string): void {
execSync("git add -A", { cwd: dir, stdio: "pipe" });
execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd: dir, stdio: "pipe" });
}
function seedMission(): void {
const missionDir = join(workspaceRoot, "missions", MISSION);
mkdirSync(join(missionDir, "evidence"), { recursive: true });
workpiece = join(missionDir, "workpiece");
mkdirSync(workpiece, { recursive: true });
write(workpiece, "src/content/system.md", "---\napp: x\n---\n");
gitInit(workpiece);
gitCommitAll(workpiece, "materialize");
write(workpiece, "src/content/pages/de/test.md", "---\ntitle: Neu\nlang: de\n---\n");
gitCommitAll(workpiece, "mission change");
const manifest = {
schemaVersion: "1.0.0",
missionId: MISSION,
systemId: SITE,
state: "open",
brief: "Test mission",
openedAt: "2026-07-30T00:00:00.000Z",
openedBy: "test-agent",
closedAt: null,
closedBy: null,
pinAtOpen: "1.0.0",
materializedAt: "2026-07-30T01:00:00.000Z",
migratedAt: null,
reconciledAt: "2026-07-30T02:00:00.000Z",
releaseId: null,
rfcId: null,
operationId: "op-001",
};
writeFileSync(join(missionDir, "mission.yaml"), JSON.stringify(manifest, null, 2) + "\n");
}
function seedCacheClone(): void {
write(
cacheClone,
"system-config.yaml",
`schemaVersion: system-config/v1\nid: ${SITE}\ncosmicStar: Vega\nmirrors:\n - path: "../systems-cache/${SITE}"\n storageType: non-bare\npinnedPlatform: "1.0.0"\nstatus: active\nregisteredAt: "2026-01-01T00:00:00Z"\nnotes: ""\n`,
);
write(
cacheClone,
"system-state.yaml",
`schemaVersion: "1.0.0"\nsystemId: ${SITE}\ncurrentMission: ${MISSION}\n`,
);
write(cacheClone, "system.pin.json", JSON.stringify({ platform: { version: "1.0.0" } }));
mkdirSync(join(cacheClone, "bordbuch"), { recursive: true });
write(cacheClone, "bordbuch/events.ndjson", "");
}
function makeInput(flags: Record): KernelCommandInput {
return { argv: [], flags } as unknown as KernelCommandInput;
}
function makeContext(): KernelRuntimeContext {
return {
io: createDefaultIO().io,
workspaceRoot,
logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, debug: () => {} },
dryRun: false,
siteExplicit: false,
outputFormat: "json",
} as unknown as KernelRuntimeContext;
}
function closeFlags(extra: Record = {}): Record {
return {
mission: MISSION,
actor: "test-agent",
"allow-no-op": true,
"skip-reconcile-check": true,
"skip-auto-sync": true,
"skip-evidence-sync": true,
...extra,
};
}
beforeEach(() => {
mockExecuteKernelCommand.mockReset();
mockRunMissionValidate.mockClear();
mockAppendBordbuch.mockClear();
testRoot = mkdtempSync(join(tmpdir(), "tmp-close-digest-"));
workspaceRoot = join(testRoot, "workspace");
cacheClone = join(testRoot, "systems-cache", SITE);
mkdirSync(workspaceRoot, { recursive: true });
mkdirSync(cacheClone, { recursive: true });
});
afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});
// ─── AC-1/AC-6: close emits + links the digest ───────────────────────────────
test("AC-6: close invokes mission.digest.generate and surfaces the path", async () => {
seedMission();
seedCacheClone();
gitInit(testRoot);
gitCommitAll(testRoot, "fixture baseline");
const calls: string[] = [];
mockExecuteKernelCommand.mockImplementation(async ({ commandName }: { commandName: string }) => {
calls.push(commandName);
if (commandName === "mission.digest.generate") {
return { exitCode: 0, data: { artifacts: [DIGEST_PATH] } };
}
return { exitCode: 0, data: {} };
});
const res = await runMissionClose(makeInput(closeFlags()), makeContext());
expect(res.exitCode).toBe(0);
expect(res.data?.state).toBe("closed");
// AC-6: close report surfaces the digest path.
expect(res.data?.closeReport.digest).toEqual({ path: DIGEST_PATH, skipped: false });
const report = JSON.parse(
readFileSync(join(workspaceRoot, "missions", MISSION, "evidence", "close-report.json"), "utf8"),
) as { digest: { path: string | null; skipped: boolean } };
expect(report.digest.path).toBe(DIGEST_PATH);
// AC-1: the close bordbuch event links the digest artifact.
const closeCall = (mockAppendBordbuch.mock.calls as unknown as unknown[][]).find(
(c) => c[2] === "mission-close" && (c[3] as string).includes("closed"),
);
expect(closeCall).toBeDefined();
const metadata = (closeCall![5] as { metadata?: Record }).metadata;
expect(metadata?.digestPath).toBe(DIGEST_PATH);
expect(metadata?.digestSkipped).toBeUndefined();
});
test("close warns but proceeds when digest generation fails (advisory)", async () => {
seedMission();
seedCacheClone();
gitInit(testRoot);
gitCommitAll(testRoot, "fixture baseline");
mockExecuteKernelCommand.mockImplementation(async ({ commandName }: { commandName: string }) => {
if (commandName === "mission.digest.generate") {
return { exitCode: 1, data: {}, summary: "workpiece missing" };
}
return { exitCode: 0, data: {} };
});
const res = await runMissionClose(makeInput(closeFlags()), makeContext());
expect(res.exitCode).toBe(0);
expect(res.data?.closeReport.digest).toEqual({ path: null, skipped: false });
});
test("--skip-digest records the audited escape hatch and skips generation", async () => {
seedMission();
seedCacheClone();
gitInit(testRoot);
gitCommitAll(testRoot, "fixture baseline");
const calls: string[] = [];
mockExecuteKernelCommand.mockImplementation(async ({ commandName }: { commandName: string }) => {
calls.push(commandName);
return { exitCode: 0, data: {} };
});
const res = await runMissionClose(makeInput(closeFlags({ "skip-digest": true })), makeContext());
expect(res.exitCode).toBe(0);
expect(calls).not.toContain("mission.digest.generate");
expect(res.data?.closeReport.digest).toEqual({ path: null, skipped: true });
const closeCall = (mockAppendBordbuch.mock.calls as unknown as unknown[][]).find(
(c) => c[2] === "mission-close" && (c[3] as string).includes("closed"),
);
const metadata = (closeCall![5] as { metadata?: Record }).metadata;
expect(metadata?.digestSkipped).toBe(true);
expect(metadata?.digestPath).toBeUndefined();
});