import type { AutopilotBlock } from "@rakazo/contracts"; import { describe, expect, it } from "vitest"; import { addAutopilotCheckpoint, advanceAutopilotGuard, autopilotContinuationPrompt, autopilotExpired, autopilotHistoryText, autopilotUntil, formatAutopilotDuration, markAutopilotLinesEnded, } from "./autopilot.js"; const started: AutopilotBlock = { kind: "autopilot", event: "autopilot.started", autopilotId: "a1", goal: "Tidy up the tickets", focus: "only tidy", until: "2026-10-05T05:00:00.000Z", }; describe("autopilot", () => { it("pauses after three runs without a checkpoint or three failures, not before", () => { let counts = { idleCount: 0, failCount: 0 }; for (const expected of [null, null, "idle"]) { const next = advanceAutopilotGuard(counts, { checkpointed: false, failed: false }); expect(next.pause).toBe(expected); counts = next; } expect(advanceAutopilotGuard(counts, { checkpointed: true, failed: false })).toEqual({ idleCount: 0, failCount: 0, pause: null, }); expect( advanceAutopilotGuard({ idleCount: 0, failCount: 2 }, { checkpointed: true, failed: true }) .pause, ).toBe("failing"); }); it("tells each continuation the order and where it stands", () => { const prompt = autopilotContinuationPrompt({ goal: "Tidy up the tickets", focus: "only tidy", until: null, lastCheckpoint: "Ticket 2 tidied", nextStep: "Ticket 3", stepCount: 2, }); expect(prompt).toMatch(/^Autopilot:/); expect(prompt).toContain("Order: Tidy up the tickets"); expect(prompt).toContain("Until: done"); expect(prompt).toContain("Steps so far: 2"); expect(prompt).toContain("Last checkpoint: Ticket 2 tidied"); }); it("ends at its time", () => { expect(autopilotExpired(null)).toBe(false); expect(autopilotExpired(new Date(1000), 2000)).toBe(true); expect(autopilotExpired(new Date(3000), 2000)).toBe(false); }); it("reads as a line in the model history and loses its controls once over", () => { expect(autopilotHistoryText(started)).toBe( "[autopilot on: Tidy up the tickets — until 2026-10-05T05:00:00.000Z — focus: only tidy]", ); const ended = markAutopilotLinesEnded([started, { kind: "text" }], "a1"); expect(ended?.[0]).toMatchObject({ ended: true }); expect(markAutopilotLinesEnded(ended ?? [], "a1")).toBeNull(); expect( autopilotHistoryText({ ...started, event: "autopilot.finished", steps: 5, text: "Tidied 5 tickets", }), ).toBe("[autopilot finished after 5 steps: Tidied 5 tickets]"); }); it("words durations briefly", () => { expect(formatAutopilotDuration(10_000)).toBe("1 min"); expect(formatAutopilotDuration(12 * 60_000)).toBe("12 min"); expect(formatAutopilotDuration(222 * 60_000)).toBe("3 h 42 min"); expect(formatAutopilotDuration(120 * 60_000)).toBe("2 h"); }); it("counts checkpoints on the start line and keeps the latest fifty", () => { let blocks: AutopilotBlock[] = [started]; for (let step = 1; step <= 52; step += 1) { blocks = addAutopilotCheckpoint(blocks, "a1", { text: `Step ${step}`, at: "t" }, step) ?? blocks; } expect(blocks[0]?.steps).toBe(52); expect(blocks[0]?.checkpoints).toHaveLength(50); expect(blocks[0]?.checkpoints?.at(-1)?.text).toBe("Step 52"); expect(addAutopilotCheckpoint(blocks, "other", { text: "x", at: "t" }, 1)).toBeNull(); }); it("picks the next morning or the next given time", () => { const evening = new Date(2026, 9, 4, 22, 30); expect(autopilotUntil({ kind: "done" }, evening)).toBeUndefined(); expect(autopilotUntil({ kind: "morning" }, evening)).toEqual(new Date(2026, 9, 5, 8, 0)); expect(autopilotUntil({ kind: "morning" }, new Date(2026, 9, 5, 2, 0))).toEqual( new Date(2026, 9, 5, 8, 0), ); expect(autopilotUntil({ kind: "time", time: "23:15" }, evening)).toEqual( new Date(2026, 9, 4, 23, 15), ); expect(autopilotUntil({ kind: "time", time: "06:00" }, evening)).toEqual( new Date(2026, 9, 5, 6, 0), ); }); });