/* Test that mission.open cleans up mission directories on bordbuch push failure. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: carried legacy entry — Bug fix: mission.open cleans up stale mission dir on bordbuch push 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 to simulate push failure vi.mock("../bordbuch/bordbuch-commit-helper.ts", () => ({ appendAndCommitBordbuch: vi.fn().mockResolvedValue({ entry: { id: "event-000001", kind: "mission-open" }, commitResult: { commitSha: "abc123", pushed: false, error: "git pull --rebase failed" }, }), })); // 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 — RFC-0951: mission.open calls runMissionMaterializeInternal vi.mock("./mission-materialize.ts", () => ({ runMissionMaterializeInternal: vi.fn().mockResolvedValue({ data: { materializedAt: "2026-01-01T00:00:00.000Z" }, summary: "materialized", nextSteps: [], }), runMissionMaterialize: vi.fn(), })); import { runMissionOpen } from "./mission-open.ts"; import { appendAndCommitBordbuch } from "../bordbuch/bordbuch-commit-helper.ts"; import { readSystemConfig, discoverSystems } from "../sternsystem/registry-io.ts"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const mockAppendAndCommit = vi.mocked(appendAndCommitBordbuch); const mockReadSystemConfig = vi.mocked(readSystemConfig); const mockDiscoverSystems = vi.mocked(discoverSystems); let testRoot: string; let workspaceRoot: string; let cacheCloneDir: string; beforeEach(async () => { testRoot = await fs.mkdtemp(path.join(os.tmpdir(), "mission-open-cleanup-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 }); // Create pin file so the pin check passes writeFileSync(path.join(cacheCloneDir, "system.pin.json"), '{"version":"1.0.0"}'); vi.clearAllMocks(); // Re-setup resolveCacheClonePath to return real dir const { resolveCacheClonePath } = await import("../sternsystem/registry-io.ts"); vi.mocked(resolveCacheClonePath).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); mockAppendAndCommit.mockResolvedValue({ entry: { id: "event-000001", kind: "mission-open" } as never, commitResult: { commitSha: "abc123", pushed: false, error: "git pull --rebase failed" }, }); }); afterEach(async () => { await fs.rm(testRoot, { recursive: true, force: true }); }); test("cleans up mission directory when bordbuch push fails", async () => { const input = { commandName: "mission.open", flags: { system: "test-system", brief: "Test mission" }, } as never; const context = { io: createDefaultIO().io, workspaceRoot, logger: { info: () => {}, success: () => {}, error: () => {}, warn: () => {}, event: () => {}, getEvents: () => [], }, } as never; await expect(runMissionOpen(input, context)).rejects.toThrow(/bordbuch push failed/); // Mission directory should not exist after cleanup const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001"); expect(existsSync(missionDir)).toBe(false); }); test("cleans up mission directory when bordbuch commit fails", async () => { mockAppendAndCommit.mockResolvedValue({ entry: { id: "event-000001", kind: "mission-open" } as never, commitResult: { commitSha: null, pushed: false, error: null }, }); const input = { commandName: "mission.open", flags: { system: "test-system", brief: "Test mission" }, } as never; const context = { io: createDefaultIO().io, workspaceRoot, logger: { info: () => {}, success: () => {}, error: () => {}, warn: () => {}, event: () => {}, getEvents: () => [], }, } as never; await expect(runMissionOpen(input, context)).rejects.toThrow(/bordbuch commit failed/); // Mission directory should not exist after cleanup const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001"); expect(existsSync(missionDir)).toBe(false); }); test("lists available systems when --system is not found", async () => { mockReadSystemConfig.mockRejectedValue(new Error("ENOENT") as never); mockDiscoverSystems.mockResolvedValue({ systems: [{ id: "warpgogol" } as never, { id: "other-system" } as never], errors: [], }); const input = { commandName: "mission.open", flags: { system: "nonexistent", brief: "Test mission" }, } as never; const context = { io: createDefaultIO().io, workspaceRoot, logger: { info: () => {}, success: () => {}, error: () => {}, warn: () => {}, event: () => {}, getEvents: () => [], }, } as never; await expect(runMissionOpen(input, context)).rejects.toThrow( /system 'nonexistent' not found. Available systems: warpgogol, other-system/, ); }); test("succeeds end-to-end when all preconditions are met", async () => { mockAppendAndCommit.mockResolvedValue({ entry: { id: "event-000001", kind: "mission-open" } as never, commitResult: { commitSha: "abc123", pushed: true, error: null }, }); const input = { commandName: "mission.open", flags: { system: "test-system", brief: "Test mission" }, } as never; const context = { io: createDefaultIO().io, workspaceRoot, logger: { info: () => {}, success: () => {}, error: () => {}, warn: () => {}, event: () => {}, getEvents: () => [], }, } as never; const result = await runMissionOpen(input, context); expect(result.data!.state).toBe("open"); expect(result.data!.systemId).toBe("test-system"); expect(result.data!.brief).toBe("Test mission"); expect(result.summary).toContain("opened and materialized mission"); // Mission directory should exist const missionDir = path.join(workspaceRoot, "missions", "test-system-m000001"); expect(existsSync(missionDir)).toBe(true); });