/* Unit tests for mission open rollback preserve covering its documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1033: initial tests for workpiece-failed preservation. */ import { test, expect, beforeEach, afterEach, vi } from "vitest"; import { existsSync, mkdirSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from "node:fs"; import path from "node:path"; import os from "node:os"; 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 }, }), })); 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"), })); vi.mock("../sternsystem/registry-io.ts", () => ({ readSystemConfig: vi.fn(), readSystemState: vi.fn(), writeSystemState: vi.fn(), resolveCacheClonePath: vi.fn(), discoverSystems: vi.fn(), })); vi.mock("../werkstatt/git-exec.ts", () => ({ gitExec: vi.fn(), })); vi.mock("../werkstatt/index.ts", () => ({ acquireLock: vi.fn(), releaseLock: vi.fn(), generateOperationId: vi.fn().mockReturnValue("op-test"), commitWerkstattSideEffects: vi.fn(), })); 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), })); vi.mock("./actor-identity.ts", () => ({ resolveActor: vi.fn().mockReturnValue("test-agent"), })); vi.mock("./mission-materialize.ts", () => ({ runMissionMaterializeInternal: vi.fn(), runMissionMaterialize: vi.fn(), })); import { runMissionOpen } from "./mission-open.ts"; import { runMissionMaterializeInternal } from "./mission-materialize.ts"; import { readSystemConfig, readSystemState, resolveCacheClonePath, discoverSystems, } from "../sternsystem/registry-io.ts"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const mockMaterializeInternal = vi.mocked(runMissionMaterializeInternal); const mockReadSystemConfig = vi.mocked(readSystemConfig); const mockReadSystemState = vi.mocked(readSystemState); const mockResolveCacheClonePath = vi.mocked(resolveCacheClonePath); const mockDiscoverSystems = vi.mocked(discoverSystems); let workspaceRoot: string; let testRoot: string; let cacheCloneDir: string; function makeContext() { return { io: createDefaultIO().io, workspaceRoot, logger: { section() {}, info() {}, warn() {}, error() {}, success() {}, }, } as never; } beforeEach(() => { testRoot = mkdtempSync(path.join(os.tmpdir(), "mission-rollback-XXXX-")); workspaceRoot = path.join(testRoot, "workspace"); cacheCloneDir = path.join(testRoot, "systems-cache", "test-system"); mkdirSync(workspaceRoot, { recursive: true }); mkdirSync(cacheCloneDir, { recursive: true }); writeFileSync(path.join(cacheCloneDir, "system.pin.json"), '{"version":"1.0.0"}'); vi.clearAllMocks(); mockResolveCacheClonePath.mockReturnValue(cacheCloneDir); 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); mockReadSystemState.mockResolvedValue({ schemaVersion: "1.0.0", systemId: "test-system", currentMission: null, lastRelease: null, lastPropagated: {}, accessPin: null, } as never); mockDiscoverSystems.mockResolvedValue({ systems: [], errors: [] } as never); mockMaterializeInternal.mockRejectedValue(new Error("materialization crashed") as never); }); afterEach(() => { rmSync(testRoot, { recursive: true, force: true }); }); test("RFC-1033: failed workpiece preserved at workpiece-failed/ with failure-report.json", async () => { 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/, ); 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); expect(existsSync(path.join(missionDir, "workpiece"))).toBe(false); const report = JSON.parse(readFileSync(path.join(missionDir, "failure-report.json"), "utf-8")); expect(report.missionId).toBe("test-system-m000001"); expect(report.systemId).toBe("test-system"); expect(report.failedStep).toBe("auto-materialize"); expect(report.error).toContain("materialization crashed"); expect(report.rolledBackAt).toBeTruthy(); expect(report.preservedAt).toContain("workpiece-failed"); }); test("RFC-1033: prior workpiece-failed/ is replaced on new failure", async () => { const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001"); mkdirSync(path.join(missionDir, "workpiece-failed", "old-content"), { recursive: true }); writeFileSync(path.join(missionDir, "workpiece-failed", "old-content", "stale.txt"), "old"); 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/, ); expect(existsSync(path.join(missionDir, "workpiece-failed"))).toBe(true); expect(existsSync(path.join(missionDir, "workpiece-failed", "old-content", "stale.txt"))).toBe( false, ); expect(existsSync(path.join(missionDir, "failure-report.json"))).toBe(true); });