/*
Test RFC-0951: mission.open auto-materializes the workpiece and rolls back on failure.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0951: test auto-materialization success and rollback on failure.
*/
import { test, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "node:fs/promises";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import os from "node:os";
// Mock bordbuch-commit-helper
vi.mock("../bordbuch/bordbuch-commit-helper.ts", () => ({
appendAndCommitBordbuch: vi.fn().mockResolvedValue({
entry: { id: "event-000001", kind: "mission-open" },
commitResult: { commitSha: "abc123", pushed: true, error: null },
}),
}));
// Mock bordbuch-io
vi.mock("../bordbuch/bordbuch-io.ts", () => ({
readBordbuch: vi.fn().mockResolvedValue([]),
deriveNextMissionNumberSafe: vi.fn().mockReturnValue(1),
validateBordbuch: vi.fn().mockResolvedValue({ entries: 0, violations: [] }),
appendBordbuchEntry: vi.fn(),
commitAndPushBordbuch: vi.fn(),
computeEntryHash: vi.fn().mockReturnValue("sha256:fake"),
}));
// Mock registry-io
vi.mock("../sternsystem/registry-io.ts", () => ({
readSystemConfig: vi.fn(),
readSystemState: vi.fn().mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: null,
lastRelease: null,
lastPropagated: {},
accessPin: null,
}),
writeSystemState: vi.fn(),
resolveCacheClonePath: vi.fn(),
discoverSystems: vi.fn(),
}));
// Mock git-exec
vi.mock("../werkstatt/git-exec.ts", () => ({
gitExec: 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", () => ({
createMissionDirectories: vi.fn(async (workspaceRoot: string, missionId: string) => {
const dir = path.join(workspaceRoot, "missions", missionId);
mkdirSync(path.join(dir, "workpiece"), { recursive: true });
mkdirSync(path.join(dir, "evidence"), { recursive: true });
}),
writeMissionManifest: vi.fn(async (workspaceRoot: string, manifest: { missionId: string }) => {
const filePath = path.join(workspaceRoot, "missions", manifest.missionId, "mission.yaml");
writeFileSync(filePath, "test: true\n");
}),
missionExists: vi.fn().mockReturnValue(false),
}));
// Mock actor-identity
vi.mock("./actor-identity.ts", () => ({
resolveActor: vi.fn().mockReturnValue("test-agent"),
}));
// Mock mission-materialize — runMissionMaterializeInternal
vi.mock("./mission-materialize.ts", () => ({
runMissionMaterializeInternal: vi.fn(),
runMissionMaterialize: vi.fn(),
}));
import { runMissionOpen } from "./mission-open.ts";
import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts";
import {
readSystemConfig,
readSystemState,
discoverSystems,
writeSystemState,
} from "../sternsystem/registry-io.ts";
import { runMissionMaterializeInternal } from "./mission-materialize.ts";
import { commitWerkstattSideEffects } from "../werkstatt/index.ts";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const mockAppendAndCommit = vi.mocked(appendAndCommitBordbuch);
const mockReadSystemConfig = vi.mocked(readSystemConfig);
const mockReadSystemState = vi.mocked(readSystemState);
const mockDiscoverSystems = vi.mocked(discoverSystems);
const mockMaterializeInternal = vi.mocked(runMissionMaterializeInternal);
const mockWriteSystemState = vi.mocked(writeSystemState);
const mockCommitWerkstatt = vi.mocked(commitWerkstattSideEffects);
let testRoot: string;
let workspaceRoot: string;
let cacheCloneDir: string;
beforeEach(async () => {
testRoot = await fs.mkdtemp(path.join(os.tmpdir(), "mission-open-auto-mat-test-"));
workspaceRoot = path.join(testRoot, "workspace");
cacheCloneDir = path.join(testRoot, "systems-cache", "test-system");
await fs.mkdir(workspaceRoot, { recursive: true });
await fs.mkdir(cacheCloneDir, { recursive: true });
writeFileSync(path.join(cacheCloneDir, "system.pin.json"), '{"version":"1.0.0"}');
vi.clearAllMocks();
const { resolveCacheClonePath } = await import("../sternsystem/registry-io.ts");
vi.mocked(resolveCacheClonePath).mockReturnValue(cacheCloneDir);
mockReadSystemState.mockResolvedValue({
schemaVersion: "1.0.0",
systemId: "test-system",
currentMission: null,
lastRelease: null,
lastPropagated: {},
accessPin: null,
} as never);
mockReadSystemConfig.mockResolvedValue({
schemaVersion: "1.0.0",
id: "test-system",
cosmicStar: "Vega",
mirrors: [{ path: "../systems-cache/test-system", storageType: "non-bare" }],
pinnedPlatform: "1.0.0",
status: "active",
registeredAt: "2026-01-01T00:00:00.000Z",
deployment: { adapter: "cloudflare-workers", channels: {} },
} as never);
mockAppendAndCommit.mockResolvedValue({
entry: { id: "event-000001", kind: "mission-open" } as never,
commitResult: { commitSha: "abc123", pushed: true, error: null },
});
mockDiscoverSystems.mockResolvedValue({ systems: [], errors: [] } as never);
});
afterEach(async () => {
await fs.rm(testRoot, { recursive: true, force: true });
});
function makeContext() {
return {
io: createDefaultIO().io,
workspaceRoot,
logger: {
info: () => {},
success: () => {},
error: () => {},
warn: () => {},
event: () => {},
getEvents: () => [],
},
} as never;
}
test("mission.open auto-materializes and returns materializedAt on success", async () => {
const materializedAt = "2026-01-15T10:00:00.000Z";
mockMaterializeInternal.mockResolvedValue({
data: {
missionId: "test-system-m000001",
systemId: "test-system",
versionComparison: {
verdict: "in-sync",
pinVersion: "1.0.0",
platformVersion: "1.0.0",
packagesDrift: false,
message: "in sync",
},
migratorChain: [],
capabilityDiff: { tier: "green", items: [] },
regeneration: { regeneratedFiles: [], success: true },
materializedAt,
preflightSkipped: false,
preflightSkipReason: null,
pipelineUsed: "build.prepare.dev",
mediaCacheWarmed: false,
mediaCacheSources: 0,
bordbuchHookInstalled: false,
artifactCacheHit: false,
artifactCacheKey: null,
artifactCacheSkipped: false,
workspaceGlobCheck: { stalePackages: [], ok: true },
},
summary: "[mission.materialize] test-system-m000001 materialized",
nextSteps: [],
} as never);
const input = {
commandName: "mission.open",
flags: { system: "test-system", brief: "Test mission" },
} as never;
const result = await runMissionOpen(input, makeContext());
expect(result.data?.materializedAt).toBe(materializedAt);
expect(result.summary).toContain("materialized");
expect(result.nextSteps?.[0]?.action).toContain("mission.validate");
expect(result.nextSteps?.[0]?.kind).toBe("optional");
expect(mockMaterializeInternal).toHaveBeenCalledOnce();
});
test("mission.open rolls back on materialization failure with compensating bordbuch entry", async () => {
mockMaterializeInternal.mockRejectedValue(new Error("codegen failed: missing template") as never);
// The rollback path calls appendAndCommitBordbuch again for mission-open-rolled-back
mockAppendAndCommit.mockResolvedValue({
entry: { id: "event-000002", kind: "mission-open-rolled-back" } as never,
commitResult: { commitSha: "def456", pushed: true, error: null },
} as never);
const input = {
commandName: "mission.open",
flags: { system: "test-system", brief: "Test mission" },
} as never;
await expect(runMissionOpen(input, makeContext())).rejects.toThrow(
/materialization failed — mission rolled back/,
);
// RFC-1033: Mission directory persists with workpiece-failed/ and failure-report.json
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
expect(existsSync(path.join(missionDir, "workpiece-failed"))).toBe(true);
expect(existsSync(path.join(missionDir, "failure-report.json"))).toBe(true);
// workpiece/ should not exist (renamed to workpiece-failed/)
expect(existsSync(path.join(missionDir, "workpiece"))).toBe(false);
// Compensating bordbuch entry was appended
expect(mockAppendAndCommit).toHaveBeenCalledTimes(2);
const rollbackCall = mockAppendAndCommit.mock.calls[1];
expect(rollbackCall?.[2]).toBe("mission-open-rolled-back");
// State was cleared
expect(mockWriteSystemState).toHaveBeenCalledWith(
workspaceRoot,
"test-system",
expect.objectContaining({ currentMission: null }),
);
// Werkstatt commit was called for cleared state
expect(mockCommitWerkstatt).toHaveBeenCalled();
});
test("mission.open rollback still throws even if compensating bordbuch entry fails", async () => {
mockMaterializeInternal.mockRejectedValue(new Error("materialization crashed") as never);
// First call (mission-open) succeeds, second call (rolled-back) fails
mockAppendAndCommit
.mockResolvedValueOnce({
entry: { id: "event-000001", kind: "mission-open" } as never,
commitResult: { commitSha: "abc123", pushed: true, error: null },
})
.mockRejectedValueOnce(new Error("bordbuch write failed") as never);
const input = {
commandName: "mission.open",
flags: { system: "test-system", brief: "Test mission" },
} as never;
await expect(runMissionOpen(input, makeContext())).rejects.toThrow(
/materialization failed — mission rolled back/,
);
// RFC-1033: Mission directory persists with workpiece-failed/ despite bordbuch failure
const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001");
expect(existsSync(path.join(missionDir, "workpiece-failed"))).toBe(true);
expect(existsSync(path.join(missionDir, "failure-report.json"))).toBe(true);
});