// @vitest-environment jsdom import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; import type { Issue, IssueCommentMetadata } from "@paperclipai/shared"; import { WorkspaceBaseRefRecoveryProvider, useWorkspaceBaseRefRecovery, useCurrentWorkspaceRetry } from "./WorkspaceBaseRefRecovery"; import { WorkspaceRetryControl } from "./WorkspaceRetryButton"; import { WorkspaceBaseRefRecoveryNotice } from "./WorkspaceBaseRefRecoveryNotice"; const mocks = vi.hoisted(() => ({ get: vi.fn(), resolve: vi.fn() })); vi.mock("../api/heartbeats", () => ({ heartbeatsApi: { get: mocks.get } })); vi.mock("../api/issues", () => ({ issuesApi: { resolveRecoveryAction: mocks.resolve } })); const issue = { id: "issue", companyId: "company", status: "blocked", assigneeAgentId: "agent", executionWorkspaceSettings: { workspaceStrategy: { baseRef: "main" } }, activeRecoveryAction: { id: "action", status: "active", cause: "configuration_incomplete", returnOwnerAgentId: "agent", evidence: { latestRunId: "run" } }, } as unknown as Issue; const metadata: IssueCommentMetadata = { version: 1, sourceRunId: "run", sections: [{ rows: [{ type: "key_value", label: "Recovery action", value: "action" }] }] }; function Consumer({ data = metadata }: { data?: IssueCommentMetadata }) { const value = useWorkspaceBaseRefRecovery(data); const retry = useCurrentWorkspaceRetry(); if (retry) return ; return value ? :

No repair

; } let container: HTMLDivElement, root: Root, client: QueryClient; beforeEach(() => { container = document.createElement("div"); document.body.append(container); root = createRoot(container); client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); mocks.resolve.mockReset(); mocks.get.mockResolvedValue({ companyId: "company", agentId: "agent", contextSnapshot: { issueId: "issue" }, resultJson: { configurationIncomplete: { reason: "workspace_base_ref_unresolved", requestedRef: "main", defaultBranch: "master" } } }); }); afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); }); async function render(data = metadata, unavailableReason: string | null = null) { await act(async () => { root.render( {}}> ); await new Promise(resolve => setTimeout(resolve, 0)); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)); }); } it("binds the repair to the exact notice and sends an atomic recovery request", async () => { mocks.resolve.mockResolvedValue({ issue: { ...issue, status: "todo", executionWorkspaceSettings: { workspaceStrategy: { baseRef: "master" } } } }); await render(); const button = [...container.querySelectorAll("button")].find(b => b.textContent === "Use master & retry")!; await act(async () => button.click()); expect(mocks.resolve).toHaveBeenCalledExactlyOnceWith("issue", { actionId: "action", outcome: "restored", sourceIssueStatus: "todo", workspaceBaseRef: { requestedRef: "main", branch: "master" } }); expect(container.textContent).toContain("Branch updated · retry requested"); }); it("does not turn an unrelated notice into a repair", async () => { await render({ ...metadata, sections: [{ rows: [{ type: "key_value", label: "Recovery action", value: "other-action" }] }] }); expect(container.textContent).toBe("No repair"); }); it("does not report success from an incomplete receipt", async () => { mocks.resolve.mockResolvedValue({ issue }); await render(); await act(async () => [...container.querySelectorAll("button")].find(b => b.textContent === "Use master & retry")!.click()); expect(container.querySelector('[role="alert"]')?.textContent).toContain("The task changed while saving"); expect(container.textContent).not.toContain("Branch updated · retry requested"); }); it("renders only the latest run notice when a repeated failure reuses the incident", async () => { await render({ ...metadata, sourceRunId: "first-failed-run" }); expect(container.textContent).toBe("No repair"); await render(metadata); expect(mocks.get).toHaveBeenCalledWith("run"); expect(container.textContent).toContain("Use master & retry"); }); it.each(["configuration_incomplete", "setup_failed"])("offers one Retry without workspace explanations for %s", async errorCode => { mocks.get.mockResolvedValue({ companyId: "company", agentId: "agent", status: "failed", errorCode, error: "Task workspace access is no longer available", contextSnapshot: { issueId: "issue" }, resultJson: { configurationIncomplete: errorCode === "configuration_incomplete" ? { reason: "workspace_source_restricted" } : undefined, executionRecovery: { kind: "bootstrap", providerWorkStarted: false } } }); const action = issue.activeRecoveryAction!; action.ownerType = "board"; let finish!: () => void; mocks.resolve.mockImplementation(() => new Promise(resolve => { finish = () => resolve({ issue: { ...issue, status: "todo" } }); })); await render({ ...metadata, sourceRunId: "earlier-failure-of-same-action" }); expect(container.textContent).toBe("Retry"); const button = container.querySelector("button")!; await act(async () => { button.click(); button.click(); }); expect(mocks.resolve).toHaveBeenCalledExactlyOnceWith("issue", { actionId: "action", outcome: "restored", sourceIssueStatus: "todo" }); expect(button.disabled).toBe(true); await act(async () => finish()); expect(container.textContent).toBe("Retrying…"); }); it("keeps retry errors actionable and allows another click", async () => { mocks.get.mockResolvedValue({ companyId: "company", agentId: "agent", status: "failed", contextSnapshot: { issueId: "issue" }, resultJson: { configurationIncomplete: { reason: "workspace_source_restricted" }, executionRecovery: { kind: "bootstrap", providerWorkStarted: false } } }); issue.activeRecoveryAction!.ownerType = "board"; mocks.resolve.mockRejectedValueOnce(new Error("The task is paused.")); await render(); await act(async () => container.querySelector("button")!.click()); expect(container.querySelector('[role="alert"]')?.textContent).toBe("The task is paused."); expect(container.querySelector("button")!.disabled).toBe(false); }); it("offers Retry even when its old notice is outside the loaded conversation", async () => { mocks.get.mockResolvedValue({ companyId: "company", agentId: "agent", status: "failed", contextSnapshot: { issueId: "issue" }, resultJson: { configurationIncomplete: { reason: "workspace_source_restricted" }, executionRecovery: { kind: "bootstrap", providerWorkStarted: false } } }); issue.activeRecoveryAction!.ownerType = "board"; await render({ version: 1, sections: [] }); expect(container.textContent).toBe("Retry"); });