import { act, fireEvent, render, screen, waitFor, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { Model, RuntimeSummary, UiState, Workspace, } from "./workspace/types"; import { initialWorkspaceState, reconcileWorkspace, workspaceReducer, } from "./workspace/reducer"; import { modelIdentityFromValue, modelIdentityValue } from "./model-catalog"; const mocks = vi.hoisted(() => ({ invoke: vi.fn(), loadSnapshot: vi.fn(), refresh: vi.fn(), dispatch: vi.fn(), emitTo: vi.fn(), useWorkspace: vi.fn(), })); vi.mock("@tauri-apps/api/core", () => ({ invoke: mocks.invoke })); vi.mock("@tauri-apps/api/event", () => ({ emitTo: mocks.emitTo })); vi.mock("@tauri-apps/api/window", () => ({ getCurrentWindow: () => ({ startDragging: vi.fn() }), })); vi.mock("@tauri-apps/plugin-dialog", () => ({ open: vi.fn() })); vi.mock("./workspace/useWorkspace", () => ({ useWorkspace: mocks.useWorkspace, })); import App from "./App"; const summary: RuntimeSummary = { runtimeId: "runtime-a", agentId: "agent-a", worktreePath: "/repo", state: "idle", label: "Session A", attention: false, queueCount: 0, lastActivity: "2026-08-24T20:00:00.000Z", openedAt: "2026-08-24T20:00:00.000Z", }; const otherSummary: RuntimeSummary = { ...summary, runtimeId: "runtime-b", agentId: "agent-b", label: "Session B", }; const uiState: UiState = { version: 1, directoryOrder: [], runtimeOrderByDirectory: {}, drafts: {}, scroll: {}, lastSeen: {}, }; function workspaceState() { const workspace: Workspace = { bridgeInstanceId: "epoch", latestSeq: 0, directories: [ { worktreePath: "/repo", isHome: true, openCount: 2, workingCount: 0, attentionCount: 0, recoveringCount: 0, errorCount: 0, runtimes: [summary, otherSummary], }, ], }; const state = reconcileWorkspace(initialWorkspaceState, workspace); state.runtimesById[summary.runtimeId].models = [ { provider: "openai", id: "gpt-5", name: "GPT 5" }, ]; return state; } function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } describe("model catalog identity", () => { it("preserves model IDs containing slashes", () => { const identity = { provider: "openrouter", modelId: "anthropic/claude/sonnet", }; expect(modelIdentityFromValue(modelIdentityValue(identity))).toEqual( identity, ); }); }); describe("App command progress and errors", () => { beforeEach(() => { vi.clearAllMocks(); mocks.invoke.mockResolvedValue(uiState); mocks.emitTo.mockResolvedValue(undefined); mocks.refresh.mockResolvedValue(undefined); mocks.loadSnapshot.mockResolvedValue(true); mocks.useWorkspace.mockReturnValue({ state: workspaceState(), dispatch: mocks.dispatch, refresh: mocks.refresh, loadSnapshot: mocks.loadSnapshot, }); }); it("shows explicit Pi readiness in the compact session status", () => { render(); const status = screen.getByRole("status", { name: "Session status" }); expect(status).toHaveTextContent("Pi ready"); expect(status).toHaveTextContent("Ready for your next prompt"); }); it("starts a session in selected directory with Ctrl+N", async () => { mocks.invoke.mockImplementation((command: string) => { if (command === "create_session_runtime") return Promise.resolve({ runtime: { runtimeId: "runtime-new" } }); return Promise.resolve(uiState); }); render(); const event = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, ctrlKey: true, key: "n", }); window.dispatchEvent(event); expect(event.defaultPrevented).toBe(true); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("create_session_runtime", { worktreePath: "/repo", }), ); }); it("loads categorized snapshot selectors and derives providers from choices", async () => { const user = userEvent.setup(); const loaded: UiState = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "gpt-5", followUpProvider: "openai", followUpModel: "gpt-5", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; mocks.invoke.mockImplementation((command: string) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") return Promise.resolve({ runtime: { runtimeId: "settings-runtime" } }); if (command === "get_session_runtime_snapshot") return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [ { provider: "openai", id: "gpt-5", name: "GPT 5" }, { provider: "openrouter", id: "anthropic/claude/sonnet", name: "Claude", }, ], }, }, }); return Promise.resolve(undefined); }); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-runtime", }), ); expect(mocks.invoke).not.toHaveBeenCalledWith("get_model_catalog"); const quickModel = screen.getByRole("button", { name: /Quick model openai · GPT 5/i, }); await user.click(quickModel); expect(screen.getByRole("group", { name: "openrouter" })).toBeInTheDocument(); await user.click( screen.getByRole("option", { name: /Claude.*anthropic\/claude\/sonnet/i }), ); await user.click( screen.getByRole("button", { name: "Default thinking Medium" }), ); await user.click(screen.getByRole("option", { name: "High" })); await user.click( screen.getByRole("button", { name: "Save Quick Pane settings" }), ); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("save_ui_state", { state: { ...loaded, quickPane: { ...loaded.quickPane, provider: "openrouter", model: "anthropic/claude/sonnet", defaultThinkingLevel: "high", }, }, }), ); }); it("uses and always closes a fresh temporary runtime whenever Settings opens", async () => { const user = userEvent.setup(); let runtimeCount = 0; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve({ ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }); if (command === "create_quick_runtime") { runtimeCount += 1; return Promise.resolve({ runtime: { runtimeId: `settings-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { const id = payload?.runtimeId === "settings-1" ? "old-model" : "new-model"; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id }], }, }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByRole("button", { name: /Quick model openai · old-model/i, }), ).toBeInTheDocument(); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-1", }), ); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByRole("button", { name: /Quick model openai · new-model/i, }), ).toBeInTheDocument(); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-2", }), ); }); it("ignores older overlapping Settings snapshot while closing both runtimes", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; const first = deferred<{ runtime: Record; extensions: never[]; models: { data: { models: { provider: string; id: string }[] } }; }>(); let runtimeCount = 0; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; return Promise.resolve({ runtime: { runtimeId: `settings-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { if (payload?.runtimeId === "settings-1") return first.promise; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "new-model" }], }, }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(runtimeCount).toBe(1)); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByRole("button", { name: /Quick model openai · new-model/i, }), ).toBeInTheDocument(); await act(async () => first.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "stale-model" }], }, }, }), ); expect(screen.queryByRole("button", { name: /stale-model/i })).toBeNull(); expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-1", }); expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-2", }); }); it("retains a stale-generation probe when its close fails", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; const staleSnapshot = deferred<{ runtime: Record; extensions: never[]; models: { data: { models: { provider: string; id: string }[] } }; }>(); let runtimeCount = 0; let staleCloseCount = 0; const lifecycle: string[] = []; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; lifecycle.push(`create:settings-${runtimeCount}`); return Promise.resolve({ runtime: { runtimeId: `settings-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { if (payload?.runtimeId === "settings-1") return staleSnapshot.promise; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "new-model" }] }, }, }); } if (command === "close_quick_runtime") { if (payload?.runtimeId === "settings-1") { staleCloseCount += 1; lifecycle.push(`close:settings-1:${staleCloseCount}`); if (staleCloseCount === 1) return Promise.reject(new Error("stale close failed")); } return Promise.resolve(undefined); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(runtimeCount).toBe(1)); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByRole("button", { name: /Quick model openai · new-model/i, }), ).toBeInTheDocument(); await act(async () => staleSnapshot.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "stale-model" }], }, }, }), ); expect( await screen.findByText( "Could not load model catalog: Error: stale close failed", ), ).toBeInTheDocument(); expect(screen.queryByRole("button", { name: /stale-model/i })).toBeNull(); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeDisabled(); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(runtimeCount).toBe(3)); expect(lifecycle.indexOf("close:settings-1:2")).toBeLessThan( lifecycle.indexOf("create:settings-3"), ); }); it("best-effort closes an in-flight Settings probe on unmount", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "model", followUpProvider: "openai", followUpModel: "model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; const snapshot = deferred(); mocks.invoke.mockImplementation((command: string) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") return Promise.resolve({ runtime: { runtimeId: "in-flight-probe" } }); if (command === "get_session_runtime_snapshot") return snapshot.promise; return Promise.resolve(undefined); }); const app = render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("get_session_runtime_snapshot", { runtimeId: "in-flight-probe", }), ); app.unmount(); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "in-flight-probe", }), ); }); it("closes temporary runtime and blocks save for snapshot failure or empty catalog", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; let openCount = 0; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { openCount += 1; return Promise.resolve({ runtime: { runtimeId: `settings-${openCount}` }, }); } if (command === "get_session_runtime_snapshot") { if (payload?.runtimeId === "settings-1") return Promise.reject(new Error("offline")); return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [] } }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByText("Could not load model catalog: Error: offline"), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeDisabled(); expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-1", }); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "settings-2", }), ); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeDisabled(); }); it("invalidates selections and refreshes catalog for an edited workspace", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; let runtimeCount = 0; mocks.invoke.mockImplementation( ( command: string, payload?: { runtimeId?: string; worktreePath?: string }, ) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; return Promise.resolve({ runtime: { runtimeId: `probe-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { const id = payload?.runtimeId === "probe-1" ? "old-model" : "new-model"; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id }], }, }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByRole("button", { name: /Quick model openai · old-model/i, }), ).toBeInTheDocument(); await waitFor(() => expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeEnabled(), ); fireEvent.change(screen.getByLabelText("Workspace"), { target: { value: "/other" }, }); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeDisabled(); expect(screen.queryByRole("button", { name: /old-model/i })).toBeNull(); await waitFor(() => expect(mocks.invoke).toHaveBeenCalledWith("create_quick_runtime", { worktreePath: "/other", }), ); expect( await screen.findByRole("button", { name: /Quick model openai · new-model/i, }), ).toBeInTheDocument(); }); it("does not reconcile current settings from an out-of-order old-workspace response", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "old-model", followUpProvider: "openai", followUpModel: "old-model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; const oldSnapshot = deferred<{ runtime: Record; extensions: never[]; models: { data: { models: Model[] } }; }>(); let runtimeCount = 0; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; return Promise.resolve({ runtime: { runtimeId: `probe-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { if (payload?.runtimeId === "probe-1") return oldSnapshot.promise; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "new-provider", id: "new-model" }], }, }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(runtimeCount).toBe(1)); fireEvent.change(screen.getByLabelText("Workspace"), { target: { value: "/other" }, }); expect( await screen.findByRole("button", { name: /Quick model new-provider · new-model/i, }), ).toBeInTheDocument(); await act(async () => oldSnapshot.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "old-provider", id: "stale-model" }], }, }, }), ); expect(screen.queryByRole("button", { name: /stale-model/i })).toBeNull(); expect(screen.getByLabelText("Workspace")).toHaveValue("/other"); expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", { runtimeId: "probe-1", }); }); it("retries a retained probe close before creating the next Settings probe", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "model", followUpProvider: "openai", followUpModel: "model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; let runtimeCount = 0; let firstProbeCloseCount = 0; const lifecycle: string[] = []; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; lifecycle.push(`create:probe-${runtimeCount}`); return Promise.resolve({ runtime: { runtimeId: `probe-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "model" }] }, }, }); if (command === "close_quick_runtime") { if (payload?.runtimeId === "probe-1") { firstProbeCloseCount += 1; lifecycle.push(`close:probe-1:${firstProbeCloseCount}`); if (firstProbeCloseCount === 1) return Promise.reject(new Error("transient close failure")); } else { lifecycle.push(`close:${payload?.runtimeId}`); } return Promise.resolve(undefined); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByText( "Could not load model catalog: Error: transient close failure", ), ).toBeInTheDocument(); expect(runtimeCount).toBe(1); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(runtimeCount).toBe(2)); await waitFor(() => expect(lifecycle).toContain("close:probe-2")); expect(lifecycle.indexOf("close:probe-1:2")).toBeLessThan( lifecycle.indexOf("create:probe-2"), ); expect(firstProbeCloseCount).toBe(2); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeEnabled(); }); it("blocks new probes and save while retained probe cleanup keeps failing", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "model", followUpProvider: "openai", followUpModel: "model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; let createCount = 0; let closeCount = 0; mocks.invoke.mockImplementation((command: string) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { createCount += 1; return Promise.resolve({ runtime: { runtimeId: "probe" } }); } if (command === "get_session_runtime_snapshot") return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "model" }] } }, }); if (command === "close_quick_runtime") { closeCount += 1; return Promise.reject(new Error("close failed")); } return Promise.resolve(undefined); }); render(); await user.click(screen.getByRole("button", { name: "Controls" })); expect( await screen.findByText("Could not load model catalog: Error: close failed"), ).toBeInTheDocument(); expect(createCount).toBe(1); expect(closeCount).toBe(1); await user.click(screen.getByRole("button", { name: "Back" })); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect(closeCount).toBe(2)); expect(createCount).toBe(1); expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeDisabled(); expect(mocks.invoke).not.toHaveBeenCalledWith( "save_ui_state", expect.anything(), ); }); it("saves only when ready catalog workspace exactly matches settings workspace", async () => { const user = userEvent.setup(); const loaded = { ...uiState, quickPane: { workspace: "/repo", provider: "openai", model: "model", followUpProvider: "openai", followUpModel: "model", instructions: "Stay concise.", maximumToolCalls: 6, defaultThinkingLevel: "medium", }, }; const nextSnapshot = deferred(); let runtimeCount = 0; mocks.invoke.mockImplementation( (command: string, payload?: { runtimeId?: string }) => { if (command === "load_ui_state") return Promise.resolve(loaded); if (command === "create_quick_runtime") { runtimeCount += 1; return Promise.resolve({ runtime: { runtimeId: `probe-${runtimeCount}` }, }); } if (command === "get_session_runtime_snapshot") { if (payload?.runtimeId === "probe-2") return nextSnapshot.promise; return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [{ provider: "openai", id: "model" }], }, }, }); } return Promise.resolve(undefined); }, ); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await waitFor(() => expect( screen.getByRole("button", { name: "Save Quick Pane settings" }), ).toBeEnabled(), ); fireEvent.change(screen.getByLabelText("Workspace"), { target: { value: "/other" }, }); const save = screen.getByRole("button", { name: "Save Quick Pane settings" }); expect(save).toBeDisabled(); fireEvent.click(save); expect(mocks.invoke).not.toHaveBeenCalledWith( "save_ui_state", expect.anything(), ); }); it("shows restart progress until the replacement snapshot is restored", async () => { const user = userEvent.setup(); const restart = deferred(); const snapshot = deferred(); mocks.invoke.mockImplementation((command: string) => { if (command === "restart") return restart.promise; if (command === "load_ui_state") return Promise.resolve(uiState); if (command === "create_quick_runtime") return Promise.resolve({ runtime: { runtimeId: "settings-runtime" } }); if (command === "get_session_runtime_snapshot") return Promise.resolve({ runtime: {}, extensions: [], models: { data: { models: [] } }, }); return Promise.resolve(undefined); }); mocks.loadSnapshot.mockReturnValue(snapshot.promise); render(); await user.click(screen.getByRole("button", { name: "Controls" })); await user.click(screen.getByRole("button", { name: "Restart Pi" })); const restarting = screen.getByRole("button", { name: "Restarting Pi…" }); expect(restarting).toBeDisabled(); expect( screen.getByText("Restoring this session and its extensions…"), ).toBeInTheDocument(); expect(screen.getByRole("main")).toHaveAttribute("aria-busy", "true"); await user.click(restarting); expect(mocks.invoke).toHaveBeenCalledWith("restart", { agentId: "agent-a" }); expect(mocks.invoke).not.toHaveBeenCalledWith("get_model_catalog"); await act(async () => restart.resolve(undefined)); expect(screen.getByRole("button", { name: "Restarting Pi…" })).toBeDisabled(); expect(mocks.loadSnapshot).toHaveBeenCalledWith("runtime-a", { throwOnError: true, }); await act(async () => snapshot.resolve(true)); await waitFor(() => expect(screen.getByRole("button", { name: "Restart Pi" })).toBeEnabled(), ); expect(screen.getByRole("main")).toHaveAttribute("aria-busy", "false"); }); it("renews a background session from its tab without changing selection", async () => { const user = userEvent.setup(); const renewal = deferred(); const snapshot = deferred(); mocks.invoke.mockImplementation((command: string) => command === "renew_session" ? renewal.promise : Promise.resolve(undefined), ); mocks.loadSnapshot.mockReturnValue(snapshot.promise); render(); await user.click(screen.getByRole("button", { name: "Renew Session B" })); expect(mocks.invoke).toHaveBeenCalledWith("renew_session", { agentId: "agent-b", }); expect( screen.getByRole("button", { name: "Renewing Session B" }), ).toBeDisabled(); expect(screen.getByRole("tab", { name: /Session A/ })).toHaveAttribute( "aria-selected", "true", ); await act(async () => renewal.resolve(undefined)); expect(mocks.loadSnapshot).toHaveBeenCalledWith("runtime-b", { throwOnError: true, }); await act(async () => snapshot.resolve(true)); await waitFor(() => expect( screen.getByRole("button", { name: "Renew Session B" }), ).toBeEnabled(), ); }); it("keeps parallel prompt submission state attached to its session", async () => { const user = userEvent.setup(); const submissionA = deferred<{ delivery: "prompt" }>(); const submissionB = deferred<{ delivery: "prompt" }>(); let state = workspaceState(); state = workspaceReducer(state, { type: "draftChanged", runtimeId: "runtime-a", draft: "Prompt A", }); mocks.dispatch.mockImplementation((action) => { state = workspaceReducer(state, action); }); mocks.useWorkspace.mockImplementation(() => ({ state, dispatch: mocks.dispatch, refresh: mocks.refresh, loadSnapshot: mocks.loadSnapshot, })); mocks.invoke.mockImplementation( (command: string, args?: { agentId?: string }) => { if (command !== "submit_prompt") return Promise.resolve(undefined); return args?.agentId === "agent-a" ? submissionA.promise : submissionB.promise; }, ); const rendered = render(); await user.click(screen.getByRole("button", { name: "Send" })); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-b", }); state = workspaceReducer(state, { type: "draftChanged", runtimeId: "runtime-b", draft: "Prompt B", }); rendered.rerender(); await user.click(screen.getByRole("button", { name: "Send" })); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-a", }); rendered.rerender(); expect(screen.getByLabelText("Prompt")).toBeDisabled(); expect(screen.getByRole("button", { name: "Sending…" })).toBeDisabled(); expect(screen.getByText("Sending your prompt…")).toBeInTheDocument(); const sessionStatus = screen.getByRole("status", { name: "Session status" }); expect(sessionStatus).toHaveTextContent("Working"); expect(sessionStatus).toHaveTextContent("Sending prompt to Pi…"); expect(sessionStatus).toHaveTextContent("Bridge"); expect(sessionStatus).toHaveTextContent("ready"); expect(sessionStatus).toHaveClass("working"); expect(rendered.container.querySelector(".status-row")).toBeNull(); await act(async () => { submissionA.resolve({ delivery: "prompt" }); submissionB.resolve({ delivery: "prompt" }); }); }); it("keeps prompt failures attached to the session that submitted them", async () => { const user = userEvent.setup(); const submission = deferred<{ delivery: "prompt" }>(); let state = workspaceState(); state = workspaceReducer(state, { type: "draftChanged", runtimeId: "runtime-a", draft: "Fail in A", }); mocks.dispatch.mockImplementation((action) => { state = workspaceReducer(state, action); }); mocks.useWorkspace.mockImplementation(() => ({ state, dispatch: mocks.dispatch, refresh: mocks.refresh, loadSnapshot: mocks.loadSnapshot, })); mocks.invoke.mockReturnValue(submission.promise); const rendered = render(); await user.click(screen.getByRole("button", { name: "Send" })); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-b", }); rendered.rerender(); await act(async () => submission.reject(new Error("session A failed"))); expect( screen.queryByText("Pi command failed: Error: session A failed"), ).toBeNull(); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-a", }); rendered.rerender(); expect( screen.getByText("Pi command failed: Error: session A failed"), ).toBeInTheDocument(); }); it("keeps command forms bound to the session that opened them", async () => { const user = userEvent.setup(); let state = workspaceState(); state = workspaceReducer(state, { type: "draftChanged", runtimeId: "runtime-a", draft: "/bash", }); mocks.dispatch.mockImplementation((action) => { state = workspaceReducer(state, action); }); mocks.useWorkspace.mockImplementation(() => ({ state, dispatch: mocks.dispatch, refresh: mocks.refresh, loadSnapshot: mocks.loadSnapshot, })); mocks.invoke.mockResolvedValue(undefined); const rendered = render(); await user.click(screen.getByRole("button", { name: /\/bash/ })); expect(screen.getByRole("dialog", { name: "/bash" })).toBeInTheDocument(); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-b", }); rendered.rerender(); expect(screen.queryByRole("dialog", { name: "/bash" })).toBeNull(); state = workspaceReducer(state, { type: "selectRuntime", runtimeId: "runtime-a", }); rendered.rerender(); await user.type(screen.getByLabelText("command"), "echo session-a"); await user.click(screen.getByRole("button", { name: "Run command" })); expect(mocks.invoke).toHaveBeenCalledWith("pi_rpc_command", { agentId: "agent-a", command: "bash", input: { command: "echo session-a" }, }); }); it("locks selected-session input and output throughout renewal", async () => { const user = userEvent.setup(); const renewal = deferred(); const snapshot = deferred(); mocks.invoke.mockImplementation((command: string) => command === "renew_session" ? renewal.promise : Promise.resolve(undefined), ); mocks.loadSnapshot.mockReturnValue(snapshot.promise); render(); const prompt = screen.getByLabelText("Prompt"); expect(prompt).toBeEnabled(); await user.click(screen.getByRole("button", { name: "Renew Session A" })); expect(prompt).toBeDisabled(); expect(screen.getByRole("tabpanel")).toHaveAttribute("aria-busy", "true"); expect( screen.getByText("Input and output are temporarily locked."), ).toBeInTheDocument(); expect( screen .getByRole("region", { name: "Conversation" }) .closest(".workflow-main"), ).toHaveAttribute("inert"); expect( screen.getByRole("button", { name: /Model Current model/i }), ).toBeDisabled(); await act(async () => renewal.resolve(undefined)); expect(prompt).toBeDisabled(); expect(mocks.loadSnapshot).toHaveBeenCalledWith("runtime-a", { throwOnError: true, }); await act(async () => snapshot.resolve(true)); await waitFor(() => expect(prompt).toBeEnabled()); expect( screen.queryByText("Input and output are temporarily locked."), ).toBeNull(); }); it("keeps the model picker open and shows a persistent alert on RPC failure", async () => { const user = userEvent.setup(); mocks.invoke.mockRejectedValue(new Error("provider rejected the model")); render(); await user.click( screen.getByRole("button", { name: /Model Current model/i }), ); await user.click(screen.getByRole("option", { name: /GPT 5/ })); expect(mocks.invoke).toHaveBeenCalledWith("set_model", { agentId: "agent-a", provider: "openai", modelId: "gpt-5", }); const modelError = await screen.findByText( "Pi model command failed: Error: provider rejected the model", ); expect(modelError.closest('[role="alert"]')).not.toBeNull(); expect( screen.getByRole("dialog", { name: "Choose model" }), ).toBeInTheDocument(); expect(mocks.loadSnapshot).not.toHaveBeenCalled(); }); });