/*
Test ADR-0084: journal sweep — findIncompleteOperationsForMission and abandonIncompleteOperationsForMission over operations/*.jsonl directories.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- ADR-0084: initial sweep tests — mission matching (missionId field, opId, filename inference), terminal-record exclusion, multi-file sweep, unreadable-file reporting, reason string.
- RFC-1311: carried legacy entry — 2026-10-04 recovery fix: excludeOpIds regression test — a still-running op listed in the exclusion set is not abandoned (mission.close must not self-abandon the ship journal it executes inside).
- RFC-1190: restore mission context + evidence archival in release-mode deploys
RFC-1190: precedence-ordered resolveMissionContext (--mission → release build-identity → currentMission), MISSION_CONTEXT_UNRESOLVED/MISMATCH blocking, fail-closed EVIDENCE_SYNC_FAILED with --skip-evidence-sync as sole escape hatch, journaled evidence-sync-* ship steps after each certify-*, evidence.sync --site/--system flag.
2026-10-04 handoff fixes: shared 25min mission.check timeout (inner 5min race + 300s orchestrator raced cold ~7-11min captures), sweep excludeOpIds so mission.close no longer self-abandons the live ship journal (broke --resume), name-keyed journal resume state so closed-mission plan shifts can't skip unexecuted steps, wrangler dev documented as the required --base-url preview server.
*/
import { test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import {
findIncompleteOperationsForMission,
abandonIncompleteOperationsForMission,
} from "../sweep.ts";
import { appendRecord, readJournal, findIncompleteOperation } from "../jsonl.ts";
let tmpDir: string;
let operationsDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(path.join(os.tmpdir(), "journal-sweep-"));
operationsDir = path.join(tmpDir, "operations");
mkdirSync(operationsDir, { recursive: true });
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
async function writeOpStarted(
journalPath: string,
opId: string,
op: string,
missionId: string,
): Promise {
await appendRecord(journalPath, {
kind: "op-started",
opId,
op,
missionId,
at: new Date().toISOString(),
platformVersion: "1.0.0",
});
}
test("abandons incomplete op whose op-started carries the closing missionId", async () => {
const journalPath = path.join(operationsDir, "ship-m-001.jsonl");
await writeOpStarted(journalPath, "leitstand-ship-1", "leitstand.ship", "m-001");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-001");
expect(result.abandoned).toHaveLength(1);
expect(result.abandoned[0].opId).toBe("leitstand-ship-1");
expect(result.unreadableFiles).toEqual([]);
const records = await readJournal(journalPath);
const abandoned = records.find((r) => r.kind === "op-abandoned");
expect(abandoned).toBeDefined();
if (abandoned && abandoned.kind === "op-abandoned") {
expect(abandoned.reason).toBe("mission closed: m-001");
}
// The op must no longer be resumable
expect(findIncompleteOperation(records)).toBeNull();
});
test("abandons incomplete op matched by filename inference when records lack missionId", async () => {
const journalPath = path.join(operationsDir, "ship-m-002.jsonl");
await writeOpStarted(journalPath, "leitstand-ship-2", "leitstand.ship", "");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-002");
expect(result.abandoned).toHaveLength(1);
expect(result.abandoned[0].opId).toBe("leitstand-ship-2");
});
test("abandons incomplete op matched by opId containing the missionId", async () => {
const journalPath = path.join(operationsDir, "ops.jsonl");
await writeOpStarted(journalPath, "deploy-m-003-abc", "custom.op", "other-mission");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-003");
expect(result.abandoned).toHaveLength(1);
expect(result.abandoned[0].opId).toBe("deploy-m-003-abc");
});
test("does not abandon ops belonging to other missions", async () => {
const journalPath = path.join(operationsDir, "ship-m-004.jsonl");
await writeOpStarted(journalPath, "leitstand-ship-4", "leitstand.ship", "m-004");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-999");
expect(result.abandoned).toEqual([]);
const records = await readJournal(journalPath);
expect(records.find((r) => r.kind === "op-abandoned")).toBeUndefined();
// Op remains resumable — untouched by the sweep
expect(findIncompleteOperation(records)).not.toBeNull();
});
test("does not abandon ops that already have a terminal record", async () => {
const journalPath = path.join(operationsDir, "ship-m-005.jsonl");
await writeOpStarted(journalPath, "leitstand-ship-5a", "leitstand.ship", "m-005");
await appendRecord(journalPath, {
kind: "op-done",
opId: "leitstand-ship-5a",
at: new Date().toISOString(),
});
await writeOpStarted(journalPath, "leitstand-ship-5b", "leitstand.ship", "m-005");
await appendRecord(journalPath, {
kind: "op-abandoned",
opId: "leitstand-ship-5b",
at: new Date().toISOString(),
reason: "manual abort",
});
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-005");
expect(result.abandoned).toEqual([]);
const records = await readJournal(journalPath);
// Only the pre-existing op-abandoned record — sweep must not append another
expect(records.filter((r) => r.kind === "op-abandoned")).toHaveLength(1);
});
test("sweeps incomplete ops across multiple journal files", async () => {
const j1 = path.join(operationsDir, "ship-m-006.jsonl");
const j2 = path.join(operationsDir, "ship-m-006-retry.jsonl");
const j3 = path.join(operationsDir, "ship-m-007.jsonl");
await writeOpStarted(j1, "op-a", "leitstand.ship", "m-006");
await writeOpStarted(j2, "op-b", "leitstand.ship", "m-006");
await writeOpStarted(j3, "op-c", "leitstand.ship", "m-007");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-006");
expect(result.abandoned.map((o) => o.opId).sort()).toEqual(["op-a", "op-b"]);
const records3 = await readJournal(j3);
expect(findIncompleteOperation(records3)).not.toBeNull();
});
test("does not match missionId as a prefix of a longer id (boundary-aware)", async () => {
const journalPath = path.join(operationsDir, "ship-m-0060.jsonl");
await writeOpStarted(journalPath, "op-m0060", "leitstand.ship", "m-0060");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-006");
expect(result.abandoned).toEqual([]);
const records = await readJournal(journalPath);
expect(findIncompleteOperation(records)).not.toBeNull();
});
test("returns empty result when operations directory does not exist", async () => {
const missing = path.join(tmpDir, "no-such-dir");
const scan = await findIncompleteOperationsForMission(missing, "m-008");
expect(scan.incomplete).toEqual([]);
expect(scan.unreadableFiles).toEqual([]);
const sweep = await abandonIncompleteOperationsForMission(missing, "m-008");
expect(sweep.abandoned).toEqual([]);
});
test("reports unreadable journal files without abandoning anything", async () => {
const corrupt = path.join(operationsDir, "ship-m-009.jsonl");
writeFileSync(corrupt, '{"kind":"op-started","opId":"op-x","op":"leitst');
const good = path.join(operationsDir, "ship-m-010.jsonl");
await writeOpStarted(good, "op-y", "leitstand.ship", "m-010");
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-009");
expect(result.unreadableFiles).toEqual(["ship-m-009.jsonl"]);
expect(result.abandoned).toEqual([]);
});
test("excludeOpIds skips still-running ops (mission.close must not self-abandon its caller)", async () => {
// Regression (2026-10-04): mission.close runs INSIDE leitstand.ship's
// journal op — the sweep saw the live ship op as incomplete and appended
// op-abandoned to it mid-run, marking the op terminal so --resume later
// reported "nothing to resume" for a genuinely failed run.
const journalPath = path.join(operationsDir, "ship-m-012.jsonl");
await writeOpStarted(journalPath, "leitstand-ship-12", "leitstand.ship", "m-012");
const excluded = new Set(["leitstand-ship-12"]);
const scan = await findIncompleteOperationsForMission(
operationsDir,
"m-012",
undefined,
excluded,
);
expect(scan.incomplete).toEqual([]);
const result = await abandonIncompleteOperationsForMission(operationsDir, "m-012", excluded);
expect(result.abandoned).toEqual([]);
const records = await readJournal(journalPath);
expect(records.find((r) => r.kind === "op-abandoned")).toBeUndefined();
expect(findIncompleteOperation(records)).not.toBeNull();
// Control: without the exclusion the same op IS abandoned
const control = await abandonIncompleteOperationsForMission(operationsDir, "m-012");
expect(control.abandoned).toHaveLength(1);
});
test("findIncompleteOperationsForMission lists incomplete ops without writing", async () => {
const journalPath = path.join(operationsDir, "ship-m-011.jsonl");
await writeOpStarted(journalPath, "op-z", "leitstand.ship", "m-011");
const scan = await findIncompleteOperationsForMission(operationsDir, "m-011");
expect(scan.incomplete).toHaveLength(1);
expect(scan.incomplete[0]).toMatchObject({ opId: "op-z", op: "leitstand.ship" });
// Read-only: no op-abandoned appended
const records = await readJournal(journalPath);
expect(records.find((r) => r.kind === "op-abandoned")).toBeUndefined();
});