/*
Test RFC-0952: defensive guards in mission.materialize for missing manifest and currentMission state.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0952: test Guard 1 (missing manifest error) and Guard 2 (currentMission auto-set).
*/
import { test, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
import path from "node:path";
import os from "node:os";
// Mock registry-io
vi.mock("../sternsystem/registry-io.ts", () => ({
readSystemConfig: vi.fn().mockResolvedValue({ status: "active" }),
readSystemState: vi.fn(),
writeSystemState: vi.fn(),
resolveCacheClonePath: vi.fn().mockReturnValue("/nonexistent-cache-clone"),
resolveMirrors: vi
.fn()
.mockReturnValue({ cachePath: "/nonexistent", gitMirrors: [], backupMirrors: [] }),
resolveMirrorPath: vi.fn().mockReturnValue(null),
}));
// Mock atomic.ts — syncCacheClone and other atomic ops
vi.mock("../werkstatt/atomic.ts", () => ({
atomicMoveDir: vi.fn(),
atomicWriteFile: vi.fn(),
resolveStagingDir: vi.fn(),
}));
// Mock werkstatt index
vi.mock("../werkstatt/index.ts", () => ({
acquireLock: vi.fn(),
releaseLock: vi.fn(),
generateOperationId: vi.fn().mockReturnValue("op-test"),
commitWerkstattSideEffects: vi.fn(),
}));
// Mock mission-io
vi.mock("./mission-io.ts", () => ({
readMissionManifest: vi.fn(),
writeMissionManifest: vi.fn(),
resolveMissionDir: vi.fn((workspaceRoot: string, missionId: string) =>
path.join(workspaceRoot, "missions", missionId),
),
}));
// Mock bordbuch
vi.mock("../bordbuch/bordbuch-hook.ts", () => ({
installBordbuchPreCommitHook: vi.fn().mockResolvedValue({ installed: false }),
}));
vi.mock("../bordbuch/bordbuch-commit-helper.ts", () => ({
appendAndCommitBordbuch: vi.fn().mockResolvedValue({
entry: { id: "event-000001", kind: "materialize" },
commitResult: { commitSha: "abc123", pushed: true, error: null },
}),
}));
vi.mock("../werkstatt/git-exec.ts", () => ({
gitExec: vi.fn(),
}));
vi.mock("../handoff/bundle-io.ts", () => ({
resolveCurrentEcosystem: vi.fn().mockResolvedValue({ version: "1.0.0" }),
resolvePlatformSemanticHash: vi.fn().mockResolvedValue("sha256:fake"),
}));
import { runMissionMaterialize } from "./mission-materialize.ts";
import { readMissionManifest } from "./mission-io.ts";
import { readSystemState, writeSystemState } from "../sternsystem/registry-io.ts";
import { acquireLock, commitWerkstattSideEffects } from "../werkstatt/index.ts";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const mockReadMissionManifest = vi.mocked(readMissionManifest);
const mockReadSystemState = vi.mocked(readSystemState);
const mockWriteSystemState = vi.mocked(writeSystemState);
const mockAcquireLock = vi.mocked(acquireLock);
const mockCommitWerkstatt = vi.mocked(commitWerkstattSideEffects);
let testRoot: string;
let workspaceRoot: string;
const validManifest = {
missionId: "test-system-m000001",
systemId: "test-system",
brief: "test brief",
state: "open",
operationId: "op-test-001",
createdAt: "2026-08-26T00:00:00Z",
updatedAt: "2026-08-26T00:00:00Z",
closedAt: null,
};
function makeContext(root: string): KernelRuntimeContext {
return {
io: createDefaultIO().io,
workspaceRoot: root,
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
env: {},
flags: {},
} as any;
}
function makeInput(missionId: string): KernelCommandInput {
return {
args: [],
flags: { mission: missionId },
} as any;
}
beforeEach(() => {
testRoot = mkdtempSync(path.join(os.tmpdir(), "mission-materialize-guards-test-"));
workspaceRoot = path.join(testRoot, "workspace");
mkdirSync(workspaceRoot, { recursive: true });
// Clear only specific mocks — vi.clearAllMocks() wipes factory-set implementations
mockReadMissionManifest.mockClear();
mockReadSystemState.mockClear();
mockWriteSystemState.mockClear();
mockAcquireLock.mockClear();
mockCommitWerkstatt.mockClear();
// acquireLock passes so execution proceeds into runMissionMaterializeInternal
mockAcquireLock.mockResolvedValue(undefined as any);
});
afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});
test("Guard 1: throws actionable error with mission.open hint when mission.yaml does not exist", async () => {
// No mission.yaml created — fs.access will fail
const error = await runMissionMaterialize(
makeInput("test-system-m000001"),
makeContext(workspaceRoot),
).catch((e: Error) => e.message);
expect(error).toContain("mission manifest not found");
expect(error).toContain("mission.open");
expect(error).toContain("--system test-system");
expect(error).toContain("missions/test-system-m000001/mission.yaml");
// readMissionManifest should NOT have been called (Guard 1 fires before it)
expect(mockReadMissionManifest).not.toHaveBeenCalled();
// writeSystemState should NOT have been called
expect(mockWriteSystemState).not.toHaveBeenCalled();
});
test("Guard 2: auto-sets currentMission when null and commits state", async () => {
// Create mission.yaml so Guard 1 passes
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
mockReadMissionManifest.mockResolvedValue(validManifest as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: null,
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
// Execution proceeds past acquireLock, syncCacheClone (no-op — no mirrors),
// Guard 2 runs, then stops at pin check (no system.pin.json in temp dir)
await expect(
runMissionMaterialize(makeInput("test-system-m000001"), makeContext(workspaceRoot)),
).rejects.toThrow("no system.pin.json");
expect(mockReadSystemState).toHaveBeenCalledWith(workspaceRoot, "test-system");
expect(mockWriteSystemState).toHaveBeenCalledWith(
workspaceRoot,
"test-system",
expect.objectContaining({ currentMission: "test-system-m000001" }),
);
expect(mockCommitWerkstatt).toHaveBeenCalledWith(
workspaceRoot,
[path.join("..", "systems-cache", "test-system", "system-state.yaml")],
expect.stringContaining("state-repair"),
);
});
test("Guard 2: auto-sets currentMission when mismatched and commits state", async () => {
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
mockReadMissionManifest.mockResolvedValue(validManifest as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: "other-system-m000099",
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
await expect(
runMissionMaterialize(makeInput("test-system-m000001"), makeContext(workspaceRoot)),
).rejects.toThrow("no system.pin.json");
expect(mockWriteSystemState).toHaveBeenCalledWith(
workspaceRoot,
"test-system",
expect.objectContaining({ currentMission: "test-system-m000001" }),
);
expect(mockCommitWerkstatt).toHaveBeenCalled();
});
test("Guard 2: does not write state when currentMission already matches", async () => {
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
mockReadMissionManifest.mockResolvedValue(validManifest as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: "test-system-m000001",
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
await expect(
runMissionMaterialize(makeInput("test-system-m000001"), makeContext(workspaceRoot)),
).rejects.toThrow("no system.pin.json");
expect(mockReadSystemState).toHaveBeenCalled();
expect(mockWriteSystemState).not.toHaveBeenCalled();
expect(mockCommitWerkstatt).not.toHaveBeenCalled();
});
test("Guard 3 (RFC-0954): blocks re-materialization when workpiece has uncommitted changes", async () => {
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
// Create a workpiece with a git repo and uncommitted changes
const workpieceDir = path.join(missionDir, "workpiece");
mkdirSync(workpieceDir, { recursive: true });
const { execSync } = await import("node:child_process");
execSync("git init -b main", { cwd: workpieceDir, stdio: "pipe" });
execSync('git config user.email "test@test.local"', { cwd: workpieceDir, stdio: "pipe" });
execSync('git config user.name "Test"', { cwd: workpieceDir, stdio: "pipe" });
execSync('git commit --allow-empty -m "initial"', { cwd: workpieceDir, stdio: "pipe" });
// Add uncommitted change
writeFileSync(path.join(workpieceDir, "content.md"), "dirty content");
// manifest.materializedAt is set — this is a re-materialization
mockReadMissionManifest.mockResolvedValue({
...validManifest,
materializedAt: "2026-08-25T00:00:00Z",
} as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: "test-system-m000001",
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
const error = await runMissionMaterialize(
makeInput("test-system-m000001"),
makeContext(workspaceRoot),
).catch((e: Error) => e.message);
expect(error).toContain("uncommitted changes");
expect(error).toContain("mission.git.commit");
expect(error).toContain("mission.reconcile");
expect(error).toContain("--force");
});
test("Guard 3 (RFC-0954): allows re-materialization with --force even when workpiece is dirty", async () => {
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
const workpieceDir = path.join(missionDir, "workpiece");
mkdirSync(workpieceDir, { recursive: true });
const { execSync } = await import("node:child_process");
execSync("git init -b main", { cwd: workpieceDir, stdio: "pipe" });
execSync('git config user.email "test@test.local"', { cwd: workpieceDir, stdio: "pipe" });
execSync('git config user.name "Test"', { cwd: workpieceDir, stdio: "pipe" });
execSync('git commit --allow-empty -m "initial"', { cwd: workpieceDir, stdio: "pipe" });
writeFileSync(path.join(workpieceDir, "content.md"), "dirty content");
mockReadMissionManifest.mockResolvedValue({
...validManifest,
materializedAt: "2026-08-25T00:00:00Z",
} as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: "test-system-m000001",
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
// With --force, guard is bypassed — execution proceeds past it
// (will still fail later at pin check, but NOT at the dirty guard)
const input = makeInput("test-system-m000001");
(input.flags as any).force = true;
await expect(runMissionMaterialize(input, makeContext(workspaceRoot))).rejects.toThrow(
"no system.pin.json",
);
});
test("Guard 3 (RFC-0954): skips guard for primary materialization (materializedAt is null)", async () => {
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
mkdirSync(missionDir, { recursive: true });
writeFileSync(path.join(missionDir, "mission.yaml"), "test: true\n");
// No workpiece directory — primary materialization
mockReadMissionManifest.mockResolvedValue(validManifest as any);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: "test-system-m000001",
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as any);
// No dirty guard error — proceeds to pin check
await expect(
runMissionMaterialize(makeInput("test-system-m000001"), makeContext(workspaceRoot)),
).rejects.toThrow("no system.pin.json");
});