import { describe, expect, it, vi } from "vitest"; import type { ExecutorDeps } from "./executor.js"; import { COMPUTER_BUSY_EXHAUSTED_ERROR, COMPUTER_BUSY_MAX_ATTEMPTS, computerBusyRetryMarker, failComputerBusyExhaustedRun, isComputerBusyRetryMarker, nextComputerBusyRetryCount, parseComputerBusyRetryCount, preserveRunErrorForComputerBusyRetry, requeueComputerBusyRun, shouldRetryComputerBusy, } from "./executor-computer-busy-retry.js"; describe("computer busy retry policy", () => { it("tracks consecutive busy retries in an internal run.error marker", () => { expect(parseComputerBusyRetryCount(null)).toBe(0); expect(parseComputerBusyRetryCount("Computer is busy")).toBe(0); expect(parseComputerBusyRetryCount(computerBusyRetryMarker(3))).toBe(3); expect(isComputerBusyRetryMarker(computerBusyRetryMarker(1))).toBe(true); expect(isComputerBusyRetryMarker("Run setup failed; retrying")).toBe(false); }); it("preserves only the internal busy-retry marker across lease claims", () => { expect(preserveRunErrorForComputerBusyRetry(null)).toBeNull(); expect(preserveRunErrorForComputerBusyRetry("Run setup failed; retrying")).toBeNull(); expect(preserveRunErrorForComputerBusyRetry(computerBusyRetryMarker(2))).toBe( computerBusyRetryMarker(2), ); }); it("allows short busy waits but stops after the configured bound", () => { expect(shouldRetryComputerBusy(0)).toBe(true); expect(shouldRetryComputerBusy(COMPUTER_BUSY_MAX_ATTEMPTS - 1)).toBe(true); expect(shouldRetryComputerBusy(COMPUTER_BUSY_MAX_ATTEMPTS)).toBe(false); expect(shouldRetryComputerBusy(COMPUTER_BUSY_MAX_ATTEMPTS + 5)).toBe(false); }); it("increments retry markers monotonically until exhausted", () => { let prior = 0; for (let attempt = 1; attempt <= COMPUTER_BUSY_MAX_ATTEMPTS; attempt += 1) { expect(shouldRetryComputerBusy(prior)).toBe(true); prior = nextComputerBusyRetryCount(prior); expect(computerBusyRetryMarker(prior)).toBe(`computer_busy_retry:${attempt}`); } expect(shouldRetryComputerBusy(prior)).toBe(false); expect(COMPUTER_BUSY_EXHAUSTED_ERROR).toMatch(/stayed busy/i); }); it("requeues while under the bound and stops enqueueing once exhausted", async () => { const enqueue = vi.fn(async () => undefined); const updateMany = vi.fn(async () => ({ count: 1 })); const deps = { prisma: { run: { updateMany } }, jobs: { enqueue }, } as unknown as ExecutorDeps; const requeued = await requeueComputerBusyRun( deps, "run-1", "worker-1", 3, COMPUTER_BUSY_MAX_ATTEMPTS - 1, null, ); expect(requeued).toBe(true); expect(enqueue).toHaveBeenCalledOnce(); expect(updateMany).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ error: computerBusyRetryMarker(COMPUTER_BUSY_MAX_ATTEMPTS), }), }), ); enqueue.mockClear(); updateMany.mockClear(); const exhausted = await requeueComputerBusyRun( deps, "run-1", "worker-1", 4, COMPUTER_BUSY_MAX_ATTEMPTS, null, ); expect(exhausted).toBe(false); expect(enqueue).not.toHaveBeenCalled(); expect(updateMany).not.toHaveBeenCalled(); }); it("fails the run with a clear error once busy retries are exhausted", async () => { const finalizeRun = vi.fn(async () => ({ continuationRunId: null })); const deps = { prisma: { attempt: { create: vi.fn(async () => ({ id: "attempt-1" })) }, bot: { findUniqueOrThrow: vi.fn(async () => ({ id: "bot-1", name: "Assistant" })) }, }, events: { finalizeRun }, jobs: { enqueue: vi.fn(async () => undefined) }, } as unknown as ExecutorDeps; await failComputerBusyExhaustedRun( deps, { id: "run-1", spaceId: "space-1", threadId: "thread-1", botId: "bot-1", taskId: "task-1", trigger: "user", sourceMessageId: null, userId: "user-1", }, "worker-1", 2, ); expect(finalizeRun).toHaveBeenCalledWith( expect.objectContaining({ outcome: "failed", error: COMPUTER_BUSY_EXHAUSTED_ERROR, attemptId: "attempt-1", }), ); }); });