import { afterEach, describe, expect, test } from "bun:test"; import { archiveRun, canArchive, canCancel, canUnarchive, cancelRun, isTerminalCancelledRun, mapError, unarchiveRun, } from "./run-actions"; type StubResponseInit = { status: number; body?: string; statusText?: string; }; function stubFetchOnce(init: StubResponseInit) { const originalFetch = globalThis.fetch; globalThis.fetch = (() => Promise.resolve( new Response(init.body ?? "", { status: init.status, statusText: init.statusText ?? "", headers: { "Content-Type": "application/json" }, }), )) as typeof fetch; return () => { globalThis.fetch = originalFetch; }; } async function expectLifecycleError( input: Promise, ): Promise<{ status: number; errors: Array<{ status: string; title: string; detail: string }> }> { try { await input; throw new Error("expected promise to reject"); } catch (error) { return error as { status: number; errors: Array<{ status: string; title: string; detail: string }> }; } } describe("run lifecycle actions", () => { let restoreFetch: (() => void) | undefined; afterEach(() => { restoreFetch?.(); restoreFetch = undefined; delete (globalThis as { window?: unknown }).window; }); test("cancelRun parses a 200 response", async () => { restoreFetch = stubFetchOnce({ status: 200, body: JSON.stringify({ id: "run-1", status: { kind: "failed", reason: "cancelled" }, created_at: "2026-04-20T12:00:00Z", }), }); const result = await cancelRun("run-1"); expect(result.status.kind).toBe("failed"); if (result.status.kind === "failed") { expect(result.status.reason).toBe("cancelled"); } }); test("archiveRun parses a 200 response", async () => { restoreFetch = stubFetchOnce({ status: 200, body: JSON.stringify({ id: "run-1", status: { kind: "archived", prior: { kind: "succeeded", reason: "completed" }, }, created_at: "2026-04-20T12:00:00Z", }), }); const result = await archiveRun("run-1"); expect(result.status.kind).toBe("archived"); }); test("unarchiveRun parses a 200 response", async () => { restoreFetch = stubFetchOnce({ status: 200, body: JSON.stringify({ id: "run-1", status: { kind: "succeeded", reason: "completed" }, created_at: "2026-04-20T12:00:00Z", }), }); const result = await unarchiveRun("run-1"); expect(result.status.kind).toBe("succeeded"); }); test("404 and 409 preserve the parsed error envelope", async () => { restoreFetch = stubFetchOnce({ status: 404, body: JSON.stringify({ errors: [{ status: "404", title: "Not Found", detail: "Run not found." }], }), }); const notFound = await expectLifecycleError(cancelRun("missing-run")); expect(notFound).toEqual({ status: 404, errors: [{ status: "404", title: "Not Found", detail: "Run not found." }], }); restoreFetch = stubFetchOnce({ status: 409, body: JSON.stringify({ errors: [{ status: "409", title: "Conflict", detail: "Run is not terminal." }], }), }); const conflict = await expectLifecycleError(archiveRun("run-1")); expect(conflict).toEqual({ status: 409, errors: [{ status: "409", title: "Conflict", detail: "Run is not terminal." }], }); }); test("non-JSON error bodies fall back to an empty error list", async () => { restoreFetch = stubFetchOnce({ status: 409, body: "conflict", statusText: "Conflict", }); const error = await expectLifecycleError(unarchiveRun("run-1")); expect(error).toEqual({ status: 409, errors: [] }); }); test("mapError returns user-facing copy for lifecycle conflicts", () => { expect(mapError({ status: 409, errors: [] }, "cancel")).toBe("This run can no longer be cancelled."); expect(mapError({ status: 409, errors: [] }, "archive")).toBe("Only terminal runs can be archived."); expect(mapError({ status: 409, errors: [] }, "unarchive")).toBe("Active runs can't be unarchived."); }); test("status predicates align with the documented run statuses", () => { expect(canCancel("submitted")).toBe(true); expect(canCancel("queued")).toBe(true); expect(canCancel("starting")).toBe(true); expect(canCancel("running")).toBe(true); expect(canCancel("paused")).toBe(true); expect(canCancel("blocked")).toBe(false); expect(canCancel("archived")).toBe(false); expect(canArchive("succeeded")).toBe(true); expect(canArchive("failed")).toBe(true); expect(canArchive("dead")).toBe(true); expect(canArchive("archived")).toBe(false); expect(canUnarchive("archived")).toBe(true); expect(canUnarchive("failed")).toBe(false); }); test("isTerminalCancelledRun distinguishes immediate cancel success from in-flight cancellation", () => { expect( isTerminalCancelledRun({ id: "run-1", status: { kind: "failed", reason: "cancelled" }, created_at: "2026-04-20T12:00:00Z", }), ).toBe(true); expect( isTerminalCancelledRun({ id: "run-1", status: { kind: "running" }, pending_control: "cancel", created_at: "2026-04-20T12:00:00Z", }), ).toBe(false); }); });