/* Unit tests for mission resume 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-1311: Compass header sweep — declares the mission suite's contract for test-file scaffold coverage. */ import { test, expect, beforeEach, afterEach, vi } from "vitest"; import { mkdtempSync, rmSync, mkdirSync } from "node:fs"; import path from "node:path"; import os from "node:os"; vi.mock("../mission-io.ts", () => ({ readMissionManifest: vi.fn(), writeMissionManifest: vi.fn(), resolveMissionDir: vi.fn((workspaceRoot: string, missionId: string) => path.join(workspaceRoot, "missions", missionId), ), })); vi.mock("../../werkstatt/index.ts", () => ({ acquireLock: vi.fn(), releaseLock: vi.fn(), isLockStale: vi.fn().mockReturnValue(false), removeStaleLock: vi.fn(), readAllLocks: vi.fn().mockResolvedValue([]), commitWerkstattSideEffects: vi.fn(), })); vi.mock("../steps/index.ts", () => ({ resolveOperationSteps: vi.fn(), })); import { runMissionResume } from "../mission-resume.ts"; import { runMissionJournalShow } from "../mission-journal-show.ts"; import { readMissionManifest } from "../mission-io.ts"; import { resolveOperationSteps } from "../steps/index.ts"; import { appendRecord } from "../../journal/index.ts"; import type { OperationStep } from "../../journal/index.ts"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; let tmpDir: string; let missionDir: string; let journalPath: string; beforeEach(() => { tmpDir = mkdtempSync(path.join(os.tmpdir(), "mission-resume-")); missionDir = path.join(tmpDir, "missions", "m-test"); journalPath = path.join(missionDir, "journal.jsonl"); mkdirSync(missionDir, { recursive: true }); vi.mocked(readMissionManifest).mockResolvedValue({ missionId: "m-test", systemId: "test-system", operationId: "op-test-123", state: "open", brief: "test", createdAt: "2026-08-27", operator: "agent", } as any); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); vi.clearAllMocks(); }); test("mission.resume with no incomplete operation returns nothing to resume", async () => { const result = await runMissionResume( { argv: [], flags: { mission: "m-test" } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.summary).toContain("nothing to resume"); }); test("mission.resume with incomplete operation calls resolveOperationSteps and runs", async () => { await appendRecord(journalPath, { kind: "op-started", opId: "op-1", op: "mission.close", missionId: "m-test", at: "2026-08-27T00:00:00.000Z", platformVersion: "1.0.0", }); await appendRecord(journalPath, { kind: "step-started", opId: "op-1", step: "s0", seq: 0, at: "t1", }); await appendRecord(journalPath, { kind: "step-done", opId: "op-1", step: "s0", seq: 0, at: "t2", }); await appendRecord(journalPath, { kind: "step-started", opId: "op-1", step: "s1", seq: 1, at: "t3", }); const steps: OperationStep[] = [ { name: "s0", run: async () => {}, verify: async () => true, }, { name: "s1", run: async () => {}, verify: async () => true, }, ]; vi.mocked(resolveOperationSteps).mockResolvedValue({ steps, ctx: { workspaceRoot: tmpDir, missionId: "m-test" }, }); const result = await runMissionResume( { argv: [], flags: { mission: "m-test" } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.data?.completed).toBe(true); expect(result.data?.resumedOp).toBe("mission.close"); }); test("RFC-1219: real resolveOperationSteps reconstructs the full reconcile ctx (m000192 path-undefined regression)", async () => { // The m000192 defect: resume passed a thin {workspaceRoot, missionId, // manifest, context} ctx into buildReconcileSteps, so fetch-and-merge // crashed with 'The "path" argument must be of type string. Received // undefined'. The resolver must now return a ReconcileStepCtx whose path // fields are all defined. const real = await vi.importActual("../steps/index.ts"); const context = { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, io: createDefaultIO().io, } as any; const resolved = await real.resolveOperationSteps( "mission.reconcile", tmpDir, "m-test", { missionId: "m-test", systemId: "test-system", state: "open" }, context, ); expect(resolved).not.toBeNull(); const ctx = resolved!.ctx as Record; for (const field of ["missionDir", "workpieceDir", "evidenceDir", "systemDir"]) { expect(typeof ctx[field], field).toBe("string"); expect((ctx[field] as string).length).toBeGreaterThan(0); } expect(ctx.workspaceRoot).toBe(tmpDir); expect(ctx.missionId).toBe("m-test"); // Step array is the real reconcile plan — fetch-and-merge must be present. const names = resolved!.steps.map((s) => s.name); expect(names).toContain("fetch-and-merge"); }); test("mission.resume --abandon without --force returns error", async () => { const result = await runMissionResume( { argv: [], flags: { mission: "m-test", abandon: true } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(1); expect(result.summary).toContain("--abandon requires --force"); }); test("mission.resume --abandon --force appends op-abandoned", async () => { await appendRecord(journalPath, { kind: "op-started", opId: "op-1", op: "mission.close", missionId: "m-test", at: "2026-08-27T00:00:00.000Z", platformVersion: "1.0.0", }); const result = await runMissionResume( { argv: [], flags: { mission: "m-test", abandon: true, force: true } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.data?.abandoned).toBe(true); }); test("mission.resume without --mission returns error", async () => { const result = await runMissionResume( { argv: [], flags: {} }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(1); expect(result.summary).toContain("--mission is required"); }); test("mission.journal.show returns records and incomplete operation info", async () => { await appendRecord(journalPath, { kind: "op-started", opId: "op-1", op: "mission.close", missionId: "m-test", at: "2026-08-27T00:00:00.000Z", platformVersion: "1.0.0", }); await appendRecord(journalPath, { kind: "step-started", opId: "op-1", step: "s0", seq: 0, at: "t1", }); const result = await runMissionJournalShow( { argv: [], flags: { mission: "m-test" } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.data?.records).toHaveLength(2); expect(result.data?.incompleteOperation).toEqual({ opId: "op-1", op: "mission.close", lastSeq: 0, status: "running", }); }); test("mission.journal.show with --op filter returns only matching records", async () => { await appendRecord(journalPath, { kind: "op-started", opId: "op-1", op: "mission.close", missionId: "m-test", at: "2026-08-27T00:00:00.000Z", platformVersion: "1.0.0", }); await appendRecord(journalPath, { kind: "op-started", opId: "op-2", op: "mission.reconcile", missionId: "m-test", at: "2026-08-27T00:00:01.000Z", platformVersion: "1.0.0", }); const result = await runMissionJournalShow( { argv: [], flags: { mission: "m-test", op: "op-2" } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.data?.records).toHaveLength(1); }); test("mission.journal.show with no journal returns empty records", async () => { const result = await runMissionJournalShow( { argv: [], flags: { mission: "m-test" } }, { workspaceRoot: tmpDir, logger: { info: () => {}, warn: () => {}, error: () => {}, success: () => {}, section: () => {}, event: () => {}, getEvents: () => [], } as any, dryRun: false, siteExplicit: false, outputFormat: "pretty", io: createDefaultIO().io, actualState: undefined as never, }, ); expect(result.exitCode).toBe(0); expect(result.data?.records).toEqual([]); expect(result.data?.incompleteOperation).toBeNull(); });