/** * @file InterruptedSessionsPrompt.test.tsx * @description The "resume interrupted sessions?" dialog: it opens only when * the server lists some, resumes only the ticked ones (never a bypass mode * carried over), and either answer dismisses the list — but a resume that * failed stays offered. * @author Nguyễn Ngọc Trí Vĩ */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import type { InterruptedRun } from "../../../lib/api"; vi.mock("../startSession", () => ({ startInFreeTabDetailed: vi.fn(async (laneId: number) => ({ runId: `agenthub-lane-${laneId}-1`, laneId, held: false, })), })); vi.mock("../../../lib/api", () => ({ api: { run: { interrupted: vi.fn(), dismissInterrupted: vi.fn(async () => ({ dismissed: 0 })), }, }, })); import { api } from "../../../lib/api"; import { startInFreeTabDetailed } from "../startSession"; import { InterruptedSessionsPrompt } from "../InterruptedSessionsPrompt"; const row = (session_id: string, lane_id: number, extra: Partial = {}) => ({ id: `agenthub-lane-${lane_id}`, session_id, lane_id, tool: "claude", cwd: "/r", model: null, permission_mode: null, effort: null, prompt_preview: `prompt ${session_id}`, started_at: "2026-10-10T00:00:00Z", ended_at: "2026-10-10T01:00:00Z", lane_title: `ws-${lane_id}`, lane_cwd: "/r", session_name: null, ...extra, }) as InterruptedRun; const list = (items: InterruptedRun[]) => vi.mocked(api.run.interrupted).mockResolvedValue({ items }); beforeEach(() => vi.clearAllMocks()); const renderIt = () => render( ); describe("InterruptedSessionsPrompt", () => { it("shows nothing when no session was interrupted", async () => { list([]); renderIt(); await waitFor(() => expect(api.run.interrupted).toHaveBeenCalled()); expect(screen.queryByRole("dialog")).toBeNull(); }); it("resumes only the ticked sessions, then dismisses the list", async () => { list([row("s-a", 1, { permission_mode: "bypassPermissions", model: "opus" }), row("s-b", 2)]); const user = userEvent.setup(); renderIt(); await screen.findByRole("dialog"); await user.click(screen.getByRole("checkbox", { name: /ws-2/ })); await user.click(screen.getByRole("button", { name: /resume 1/i })); await waitFor(() => expect(api.run.dismissInterrupted).toHaveBeenCalledWith(["s-a", "s-b"])); expect(startInFreeTabDetailed).toHaveBeenCalledTimes(1); // The model goes back; a bypass mode is never carried over. expect(startInFreeTabDetailed).toHaveBeenCalledWith(1, { resumeSessionId: "s-a", model: "opus", }); await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull()); }); it("resumes nothing on Don't resume, and keeps a session whose resume failed", async () => { list([row("s-a", 1)]); const user = userEvent.setup(); const { unmount } = renderIt(); await user.click(await screen.findByText("Don't resume")); await waitFor(() => expect(api.run.dismissInterrupted).toHaveBeenCalledWith(["s-a"])); expect(startInFreeTabDetailed).not.toHaveBeenCalled(); unmount(); vi.clearAllMocks(); list([row("s-a", 1), row("s-b", 2)]); vi.mocked(startInFreeTabDetailed).mockRejectedValueOnce(new Error("boom")); renderIt(); await user.click(await screen.findByRole("button", { name: /resume 2/i })); await waitFor(() => expect(api.run.dismissInterrupted).toHaveBeenCalledWith(["s-b"])); expect(screen.getByRole("checkbox", { name: /ws-1/ })).toBeInTheDocument(); }); });