import { describe, it, expect, vi } from "vitest"; import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { DIProvider } from "@/app/di"; import { MockAgentGateway, MockLayoutGateway, MockSystemGateway, MockWorkStateGateway, } from "@/adapters/mock"; import { leaves } from "@/features/layout/layout"; import type { Gateways } from "@/ports"; import { ProjectWorkStatePanel, WorkStatePanelErrorBoundary, } from "./ProjectWorkStatePanel"; const PROJECT_ID = "project-work-state-test"; function renderPanel( workState: MockWorkStateGateway = new MockWorkStateGateway(), system: MockSystemGateway = new MockSystemGateway(), overrides: Partial = {}, ) { const gateways = { workState, system, ...overrides } as unknown as Gateways; return { workState, system, ...render( , ), }; } describe("ProjectWorkStatePanel", () => { it("renders the empty state", async () => { renderPanel(); expect(await screen.findByText("No agent work state.")).toBeTruthy(); }); it("renders an idle offline agent", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-1", name: "Planner", profileId: "codex", busy: { state: "idle" }, }, ], }); renderPanel(workState); expect(await screen.findByText("Planner")).toBeTruthy(); expect(screen.getByText("Offline")).toBeTruthy(); expect(screen.getByText("Idle")).toBeTruthy(); expect(screen.queryByLabelText("Planner tickets")).toBeNull(); }); it("renders a live busy agent with a short ticket", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-2", name: "Builder", profileId: "claude", live: { nodeId: "node-a", sessionId: "session-a", kind: "pty", }, busy: { state: "busy", ticket: "4c65d981-da5e-412d-85ed-3b9779b670fa", sinceMs: 123, }, }, ], }); renderPanel(workState); expect(await screen.findByText("Builder")).toBeTruthy(); expect(screen.getByText("Live")).toBeTruthy(); expect(screen.getByText("Busy")).toBeTruthy(); expect(screen.getByLabelText("busy ticket 4c65d981")).toBeTruthy(); }); it("renders an in-progress human ticket", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-4", name: "Implementer", profileId: "codex", busy: { state: "busy", ticket: "ticket-in-progress-abcdef", sinceMs: 789, }, tickets: [ { ticketId: "ticket-in-progress-abcdef", conversationId: "conversation-a", position: 0, status: "inProgress", source: { kind: "human" }, requesterLabel: "Anthony", taskPreview: "Wire the workstate queue UI", taskLen: 36, }, ], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Implementer tickets"); expect(within(list).getByText("#1 In progress")).toBeTruthy(); expect(within(list).getByText("Human")).toBeTruthy(); expect(within(list).getByText("Wire the workstate queue UI")).toBeTruthy(); expect(within(list).getByLabelText("ticket ticket-i")).toBeTruthy(); expect(within(list).getByLabelText("9 more characters")).toBeTruthy(); }); it("renders two tickets in FIFO order", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-5", name: "Reviewer", profileId: "claude", busy: { state: "idle" }, tickets: [ { ticketId: "ticket-second-abcdef", conversationId: "conversation-b", position: 1, status: "queued", source: { kind: "agent", agentId: "agent-main-123456" }, requesterLabel: "Main", taskPreview: "Then review the result", taskLen: 22, }, { ticketId: "ticket-first-abcdef", conversationId: "conversation-a", position: 0, status: "inProgress", source: { kind: "agent", agentId: "agent-qa-123456" }, requesterLabel: "QA", taskPreview: "Check behavior", taskLen: 14, }, ], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Reviewer tickets"); expect(within(list).getByText("QA (agent-qa)")).toBeTruthy(); expect(within(list).getByText("Main (agent-ma)")).toBeTruthy(); expect(list.textContent?.indexOf("#1 In progress")).toBeLessThan( list.textContent?.indexOf("#2 Queued") ?? -1, ); }); it("normalizes the real backend background-task payload shape", async () => { // Mirrors AgentBackgroundTaskStateDto (crates/app-tauri/src/dto.rs): camelCase, // `state` (not `status`), optional exitCode/summary/stdoutTail/stderrTail omitted // when absent, createdAtMs/updatedAtMs present, NO ownerAgentId/projectId/finishedAtMs. const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-bg", name: "Runner", profileId: "codex", busy: { state: "idle" }, backgroundTasks: [ { taskId: "task-waiting-1", kind: "command", state: "waiting", createdAtMs: 10, updatedAtMs: 11, }, { taskId: "task-queued-1", kind: "headlessRendezvous", state: "queued", createdAtMs: 12, updatedAtMs: 13, }, { taskId: "task-failed-1", kind: "command", state: "failed", exitCode: 1, summary: "process failed", stderrTail: "boom", createdAtMs: 14, updatedAtMs: 15, }, { taskId: "task-expired-1", kind: "sessionResume", state: "expired", createdAtMs: 16, updatedAtMs: 17, }, ], }, ], }); // Adapter-level assertion: locks the mapping against the real payload. const state = await workState.getProjectWorkState(PROJECT_ID); const tasks = state.agents[0]?.backgroundTasks ?? []; expect(tasks.map((t) => [t.taskId, t.status])).toEqual([ ["task-waiting-1", "pending"], ["task-queued-1", "pending"], ["task-failed-1", "failed"], ["task-expired-1", "cancelled"], ]); // Optional fields omitted by the backend normalize to null (not undefined/NaN). expect(tasks[0]?.exitCode).toBeNull(); expect(tasks[0]?.stdoutTail).toBeNull(); expect(tasks[0]?.summary).toBeNull(); expect(tasks[2]?.stderrTail).toBe("boom"); // `summary` (ticket #5) is carried through when present. expect(tasks[2]?.summary).toBe("process failed"); // updatedAtMs (backend-emitted) is carried through for chronological ordering. expect(tasks.map((t) => t.updatedAtMs)).toEqual([11, 13, 15, 17]); renderPanel(workState); const list = await screen.findByLabelText("Runner background tasks"); const cancelButtons = within(list).getAllByRole("button", { name: "Cancel" }); const retryButtons = within(list).getAllByRole("button", { name: "Retry" }); // waiting + queued are cancellable; failed + expired are retryable. expect(cancelButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2); expect(retryButtons.filter((b) => !(b as HTMLButtonElement).disabled)).toHaveLength(2); // Rows are ordered most-recently-updated first (updatedAtMs desc), taskId as // the deterministic tiebreak — locks the fix for the dead finishedAtMs sort. const text = list.textContent ?? ""; const order = ["task-exp", "task-fai", "task-que", "task-wai"].map((id) => text.indexOf(id), ); expect(order).toEqual([...order].sort((a, b) => a - b)); expect(order.every((i) => i >= 0)).toBe(true); }); it("shows a terminal task summary and hides it when absent or non-terminal (#5)", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-summary", name: "Reporter", profileId: "codex", busy: { state: "idle" }, backgroundTasks: [ { taskId: "run-with-summary", kind: "command", state: "running", summary: "should-not-appear-while-running", createdAtMs: 40, updatedAtMs: 40, }, { taskId: "done-with-summary", kind: "command", state: "completed", summary: "Build succeeded in 12s", createdAtMs: 30, updatedAtMs: 30, }, { taskId: "fail-with-summary", kind: "command", state: "failed", exitCode: 1, summary: "Compile error: missing semicolon", createdAtMs: 20, updatedAtMs: 20, }, { taskId: "done-no-summary", kind: "command", state: "completed", createdAtMs: 10, updatedAtMs: 10, }, ], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Reporter background tasks"); // Terminal tasks with a summary render it discreetly. expect( within(list).getByLabelText("task done-wit summary").textContent, ).toBe("Build succeeded in 12s"); expect( within(list).getByText("Compile error: missing semicolon"), ).toBeTruthy(); // A completed task without a summary shows no summary line. expect(within(list).queryByLabelText("task done-no- summary")).toBeNull(); // A still-running task never shows its (non-terminal) summary. expect(within(list).queryByLabelText("task run-with summary")).toBeNull(); expect( within(list).queryByText("should-not-appear-while-running"), ).toBeNull(); }); it("renders a legacy agent without tickets", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-6", name: "Legacy", profileId: "gemini", busy: { state: "idle" }, }, ], }); renderPanel(workState); expect(await screen.findByText("Legacy")).toBeTruthy(); expect(screen.getByText("Offline")).toBeTruthy(); expect(screen.getByText("Idle")).toBeTruthy(); expect(screen.queryByLabelText("Legacy tickets")).toBeNull(); }); it("ignores a stray top-level backgroundTasks array (nested tasks intact) (#5)", async () => { // The backend nests tasks under their agent and emits no `ownerAgentId`; a // project-root `backgroundTasks` array is not part of the contract and must // not be merged in (dead path removed in ticket #5). const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-nested", name: "Nested", profileId: "codex", busy: { state: "idle" }, backgroundTasks: [ { taskId: "nested-1", kind: "command", state: "completed", createdAtMs: 1, updatedAtMs: 2, }, ], }, ], backgroundTasks: [ { taskId: "orphan-top-level", kind: "command", state: "completed", ownerAgentId: "agent-nested", createdAtMs: 3, updatedAtMs: 4, }, ], }); const state = await workState.getProjectWorkState(PROJECT_ID); expect((state.agents[0]?.backgroundTasks ?? []).map((t) => t.taskId)).toEqual( ["nested-1"], ); }); it("normalizes missing top-level work state arrays", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, {}); await expect(workState.getProjectWorkState(PROJECT_ID)).resolves.toEqual({ agents: [], conversations: [], }); }); it("renders an error fallback instead of blanking the app when Work throws", () => { function BrokenWorkPanel(): JSX.Element { throw new Error("render exploded"); } render( , ); expect(screen.getByRole("alert").textContent).toContain( "Work panel failed to render.", ); expect(screen.getByText("render exploded")).toBeTruthy(); expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); }); it("renders conversation objective and summary joined by conversation id", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-8", name: "Summarizer", profileId: "codex", busy: { state: "busy", ticket: "ticket-goal-abcdef", sinceMs: 123 }, tickets: [ { ticketId: "ticket-goal-abcdef", conversationId: "conversation-goal", position: 0, status: "inProgress", source: { kind: "human" }, requesterLabel: "Anthony", taskPreview: "Continue implementation", taskLen: 23, }, { ticketId: "ticket-summary-abcdef", conversationId: "conversation-summary", position: 1, status: "queued", source: { kind: "agent", agentId: "agent-main-123456" }, requesterLabel: "Main", taskPreview: "Follow up", taskLen: 9, }, ], }, ], conversations: [ { conversationId: "conversation-summary", status: "ready", objectivePreview: null, summaryPreview: "Previous turn established the adapter boundary.", summaryLen: 47, upTo: "turn-2", recentTurns: [], }, { conversationId: "conversation-goal", status: "ready", objectivePreview: "Ship the Work panel summaries", summaryPreview: "This should be lower priority than the goal.", summaryLen: 48, upTo: "turn-1", recentTurns: [], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Summarizer tickets"); expect(within(list).getAllByText("Summary")).toHaveLength(2); expect( within(list).getByText("Goal: Ship the Work panel summaries"), ).toBeTruthy(); expect( within(list).getByText( "Previous turn established the adapter boundary.", ), ).toBeTruthy(); }); it("keeps partial and unavailable summaries compact without hiding tickets", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-9", name: "Contextualizer", profileId: "claude", busy: { state: "idle" }, tickets: [ { ticketId: "ticket-partial-abcdef", conversationId: "conversation-partial", position: 0, status: "inProgress", source: { kind: "agent", agentId: "agent-qa-123456" }, requesterLabel: "QA", taskPreview: "Validate partial context", taskLen: 24, }, { ticketId: "ticket-unavailable-abcdef", conversationId: "conversation-unavailable", position: 1, status: "queued", source: { kind: "human" }, requesterLabel: "Anthony", taskPreview: "Validate unavailable context", taskLen: 28, }, ], }, ], conversations: [ { conversationId: "conversation-partial", status: "partial", objectivePreview: null, summaryPreview: null, summaryLen: 0, upTo: null, recentTurns: [ { role: "prompt", source: { kind: "agent", agentId: "agent-qa-123456" }, atMs: 100, textPreview: "Only the latest prompt was available.", textLen: 37, }, ], }, { conversationId: "conversation-unavailable", status: "unavailable", objectivePreview: null, summaryPreview: "Summary service unavailable right now.", summaryLen: 38, upTo: null, recentTurns: [], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Contextualizer tickets"); expect(within(list).getByText("#1 In progress")).toBeTruthy(); expect(within(list).getByText("Validate partial context")).toBeTruthy(); expect(within(list).getByText("Partial")).toBeTruthy(); expect( within(list).getByText("Last: Only the latest prompt was available."), ).toBeTruthy(); expect(within(list).getByText("#2 Queued")).toBeTruthy(); expect(within(list).getByText("Validate unavailable context")).toBeTruthy(); expect(within(list).getByText("Unavailable")).toBeTruthy(); expect( within(list).getByText("Summary service unavailable right now."), ).toBeTruthy(); }); it("refreshes when a relevant domain event fires", async () => { const workState = new MockWorkStateGateway(); const system = new MockSystemGateway(); renderPanel(workState, system); await screen.findByText("No agent work state."); const spy = vi.spyOn(workState, "getProjectWorkState"); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-3", name: "Responder", profileId: "gemini", busy: { state: "busy", ticket: "ticket-123456789", sinceMs: 456 }, }, ], }); system.emit({ type: "agentBusyChanged", agentId: "agent-3", busy: true, }); expect(await screen.findByText("Responder")).toBeTruthy(); await waitFor(() => expect(spy).toHaveBeenCalled()); }); it("refreshes when delegationReady fires", async () => { const workState = new MockWorkStateGateway(); const system = new MockSystemGateway(); renderPanel(workState, system); await screen.findByText("No agent work state."); const spy = vi.spyOn(workState, "getProjectWorkState"); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-7", name: "Delegatee", profileId: "codex", busy: { state: "busy", ticket: "ticket-ready-123456", sinceMs: 123 }, tickets: [ { ticketId: "ticket-ready-123456", conversationId: "conversation-ready", position: 0, status: "inProgress", source: { kind: "agent", agentId: "agent-main-123456" }, requesterLabel: "Main", taskPreview: "Handle this delegated task", taskLen: 26, }, ], }, ], }); system.emit({ type: "delegationReady", agentId: "agent-7", ticket: "ticket-ready-123456", text: "Handle this delegated task", }); expect(await screen.findByText("Delegatee")).toBeTruthy(); expect(screen.getByText("#1 In progress")).toBeTruthy(); await waitFor(() => expect(spy).toHaveBeenCalled()); }); it("Open focuses a visible live cell and does not launch", async () => { const workState = new MockWorkStateGateway(); const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id; const host = document.createElement("div"); host.dataset.nodeId = leafId; document.body.appendChild(host); const launchSpy = vi.spyOn(agent, "launchAgent"); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-open", name: "Visible", profileId: "codex", live: { nodeId: leafId, sessionId: "session-open", kind: "pty" }, busy: { state: "idle" }, tickets: [], }, ], conversations: [], }); renderPanel(workState, new MockSystemGateway(), { layout, agent }); fireEvent.click(await screen.findByRole("button", { name: "Open" })); expect(host.style.outline).toContain("2px solid"); expect(launchSpy).not.toHaveBeenCalled(); host.remove(); }); it("Attach rebinds a hidden live agent to the deterministic empty cell", async () => { const workState = new MockWorkStateGateway(); const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const created = await agent.createAgent(PROJECT_ID, { name: "Hidden", profileId: "codex", }); await agent.launchAgent( PROJECT_ID, created.id, { cwd: "/x", rows: 24, cols: 80, nodeId: "hidden-node" }, () => {}, ); const attachSpy = vi.spyOn(agent, "attachLiveAgent"); const launchSpy = vi.spyOn(agent, "launchAgent"); const target = leaves(await layout.loadLayout(PROJECT_ID))[0].id; workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: created.id, name: "Hidden", profileId: "codex", live: { nodeId: "hidden-node", sessionId: "mock-agent-session-1", kind: "pty" }, busy: { state: "idle" }, tickets: [], }, ], conversations: [], }); renderPanel(workState, new MockSystemGateway(), { layout, agent }); const attach = await screen.findByRole("button", { name: "Attach" }); await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(false)); fireEvent.click(attach); await waitFor(() => expect(attachSpy).toHaveBeenCalledWith(PROJECT_ID, created.id, target), ); await waitFor(async () => { const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0]; expect(leaf.agent).toBe(created.id); expect(leaf.session).toBe("mock-agent-session-1"); }); expect(launchSpy).not.toHaveBeenCalled(); }); it("disables Attach when no visible target cell exists", async () => { const workState = new MockWorkStateGateway(); const layout = new MockLayoutGateway(); const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id; await layout.mutateLayout(PROJECT_ID, { type: "setCellAgent", target: leafId, agent: "other-agent", }); await layout.mutateLayout(PROJECT_ID, { type: "setSession", target: leafId, session: "occupied-session", }); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-hidden", name: "Hidden", profileId: "codex", live: { nodeId: "hidden-node", sessionId: "session-hidden", kind: "pty" }, busy: { state: "idle" }, tickets: [], }, ], conversations: [], }); renderPanel(workState, new MockSystemGateway(), { layout, agent: new MockAgentGateway(), }); const attach = await screen.findByRole("button", { name: "Attach" }); await waitFor(() => expect((attach as HTMLButtonElement).disabled).toBe(true)); expect(await screen.findByText("No empty visible cell.")).toBeTruthy(); }); it("Stop confirms with a busy warning, stops the live agent, and refreshes Work", async () => { const workState = new MockWorkStateGateway(); const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const created = await agent.createAgent(PROJECT_ID, { name: "Runner", profileId: "codex", }); const leafId = leaves(await layout.loadLayout(PROJECT_ID))[0].id; await agent.launchAgent( PROJECT_ID, created.id, { cwd: "/x", rows: 24, cols: 80, nodeId: leafId }, () => {}, ); await layout.mutateLayout(PROJECT_ID, { type: "setCellAgent", target: leafId, agent: created.id, }); await layout.mutateLayout(PROJECT_ID, { type: "setSession", target: leafId, session: "mock-agent-session-1", }); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: created.id, name: "Runner", profileId: "codex", live: { nodeId: leafId, sessionId: "mock-agent-session-1", kind: "pty" }, busy: { state: "busy", ticket: "ticket-stop", sinceMs: 1 }, tickets: [ { ticketId: "ticket-stop", conversationId: "conv-stop", position: 0, status: "inProgress", source: { kind: "human" }, requesterLabel: "Human", taskPreview: "Do work", taskLen: 7, }, ], }, ], conversations: [], }); const stopSpy = vi.spyOn(agent, "stopLiveAgent"); const refreshSpy = vi.spyOn(workState, "getProjectWorkState"); const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true); renderPanel(workState, new MockSystemGateway(), { layout, agent }); fireEvent.click(await screen.findByRole("button", { name: "Stop" })); await waitFor(() => expect(confirmSpy).toHaveBeenCalledWith( "Stop Runner? The current turn will be interrupted.", ), ); await waitFor(() => expect(stopSpy).toHaveBeenCalledWith(PROJECT_ID, created.id)); await waitFor(() => expect(refreshSpy).toHaveBeenCalled()); await waitFor(async () => { const leaf = leaves(await layout.loadLayout(PROJECT_ID))[0]; expect(leaf.session).toBeNull(); }); confirmSpy.mockRestore(); }); it("View conversation exposes only Lot C preview fields", async () => { const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-view", name: "Viewer", profileId: "codex", busy: { state: "idle" }, tickets: [ { ticketId: "ticket-view", conversationId: "conversation-view", position: 0, status: "queued", source: { kind: "human" }, requesterLabel: "Human", taskPreview: "Inspect preview", taskLen: 15, }, ], }, ], conversations: [ { conversationId: "conversation-view", status: "ready", objectivePreview: "Keep it compact", summaryPreview: "Only the exposed summary preview.", summaryLen: 33, upTo: "internal-turn-id", recentTurns: [ { role: "response", source: { kind: "agent", agentId: "agent-view" }, atMs: 42, textPreview: "Recent exposed turn.", textLen: 20, }, ], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Viewer tickets"); fireEvent.click(within(list).getByRole("button", { name: "View" })); expect(within(list).getByText("Goal:")).toBeTruthy(); expect(within(list).getByText("Keep it compact")).toBeTruthy(); expect(within(list).getByText("Summary:")).toBeTruthy(); expect( within(list).getByText("Only the exposed summary preview."), ).toBeTruthy(); expect(within(list).getByText("response:")).toBeTruthy(); expect(within(list).getByText("Recent exposed turn.")).toBeTruthy(); expect(list.textContent).not.toContain("internal-turn-id"); }); it("Copy summary copies only the exposed preview text", async () => { const writeText = vi.fn().mockResolvedValue(undefined); vi.stubGlobal("navigator", { clipboard: { writeText } }); const workState = new MockWorkStateGateway(); workState._setProjectWorkState(PROJECT_ID, { agents: [ { agentId: "agent-copy", name: "Copier", profileId: "codex", busy: { state: "idle" }, tickets: [ { ticketId: "ticket-copy", conversationId: "conversation-copy", position: 0, status: "queued", source: { kind: "human" }, requesterLabel: "Human", taskPreview: "Copy preview", taskLen: 12, }, ], }, ], conversations: [ { conversationId: "conversation-copy", status: "ready", objectivePreview: null, summaryPreview: "Copy this summary preview only.", summaryLen: 31, upTo: null, recentTurns: [ { role: "prompt", source: { kind: "human" }, atMs: 1, textPreview: "Do not copy this turn.", textLen: 22, }, ], }, ], }); renderPanel(workState); const list = await screen.findByLabelText("Copier tickets"); fireEvent.click(within(list).getByRole("button", { name: "Copy" })); await waitFor(() => expect(writeText).toHaveBeenCalledWith("Copy this summary preview only."), ); expect(await within(list).findByRole("button", { name: "Copied" })).toBeTruthy(); vi.unstubAllGlobals(); }); });