import type { ReactNode } from "react"; import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Message } from "@/lib/types/http"; const launchError = { stamp: "task-wide-launch-error", occurred_at: "2026-08-20T10:00:00Z", preview: "The workspace could not be prepared.", category: "workspace_checkout_failed", }; const messageListRecoveryRevealKeys = vi.hoisted(() => [] as Array); const priorTranscriptMessage = { id: "prior-transcript", author_type: "agent", content: "The prior session stopped after its checkout failed.", type: "status", created_at: "2026-08-19T10:00:00Z", } as unknown as Message; const appStoreState = { userSettings: { showAnchoredPromptBar: false, showScrollToLastPrompt: false, showScrollToStart: false, }, taskSessions: { items: { "prior-session": { name: null, agent_profile_id: null, last_read_message_id: null }, }, }, agentProfiles: { items: [] }, messages: { bySession: { "prior-session": [priorTranscriptMessage] } }, }; const panelState = { resolvedSessionId: "prior-session", session: { state: "FAILED", error_message: "The prior session stopped.", metadata: null as Record | null, }, taskId: "task-1", isWorking: false, messagesLoading: false, historyRefreshPending: false, isInitialMessagesLoading: false, groupedItems: [], allMessages: [priorTranscriptMessage], footerActionMessages: [], permissionsByToolCallId: new Map(), childrenByParentToolCallId: new Map(), agentMessageCount: 0, pendingClarification: null, pendingClarificationGroup: null, }; vi.mock("./panel-primitives", () => ({ PanelRoot: ({ children }: { children: ReactNode }) =>
{children}
, PanelBody: ({ children }: { children: ReactNode }) =>
{children}
, })); vi.mock("@/components/state-provider", () => ({ useAppStore: (selector: (state: typeof appStoreState) => unknown) => selector(appStoreState), useAppStoreApi: () => ({ getState: () => appStoreState, }), })); vi.mock("@/hooks/domains/settings/use-settings-data", () => ({ useSettingsData: () => undefined, })); vi.mock("@/hooks/use-responsive-breakpoint", () => ({ useResponsiveBreakpoint: () => ({ isMobile: false }), })); vi.mock("./task-archived-context", () => ({ useIsTaskArchived: () => false, })); vi.mock("./task-launch-error-context", () => ({ useTaskLaunchErrorContext: () => ({ taskId: "task-1", workspaceId: "workspace-1", statusSummary: { active_error: launchError }, repositories: [], }), })); vi.mock("@/hooks/domains/task/use-task-status-summary", () => ({ useTaskStatusSummary: () => ({ active_error: launchError }), })); vi.mock("./chat/use-chat-panel-state", () => ({ useChatPanelState: () => panelState, })); vi.mock("./chat/chat-input-area", () => ({ ChatInputArea: ({ launchErrorOwned, panelState: state, }: { launchErrorOwned?: boolean; panelState: typeof panelState; }) => (
{!launchErrorOwned && state.session?.state === "FAILED" && (
prior session recovery
)}
), useSubmitHandler: () => ({ isSending: false, handleSubmit: vi.fn() }), useChatPanelHandlers: () => ({ handleCancelTurn: vi.fn() }), })); vi.mock("@/components/task/chat/message-list", () => ({ MessageList: ({ messages, launchErrorOwned, prependContent, recoveryRevealKey, }: { messages: Message[]; launchErrorOwned?: boolean; prependContent?: ReactNode; recoveryRevealKey?: string | null; }) => { messageListRecoveryRevealKeys.push(recoveryRevealKey); return (
{prependContent} {!launchErrorOwned && messages.map((message) =>
{message.content}
)}
); }, })); vi.mock("./simple/components/task-chat-launch-error", () => ({ TaskChatLaunchError: ({ statusSummary, }: { statusSummary?: { active_error?: typeof launchError }; }) => statusSummary?.active_error ? (
{statusSummary.active_error.preview}
) : null, })); vi.mock("@/components/shared/task-markdown-file-link-provider", () => ({ TaskMarkdownFileLinkProvider: ({ children }: { children: ReactNode }) => <>{children}, })); vi.mock("./chat/clarification-panel-section", () => ({ ClarificationPanelSection: () => null, })); vi.mock("./chat/use-composer-agent-start-hint", () => ({ useComposerAgentStartHint: () => false, })); vi.mock("@/hooks/use-panel-search", () => ({ usePanelSearch: () => undefined, })); vi.mock("@/hooks/domains/session/use-session-search", () => ({ useSessionSearch: () => ({ isOpen: false, query: "", hits: [], activeHitId: null, isSearching: false, setQuery: vi.fn(), setActiveHit: vi.fn(), open: vi.fn(), close: vi.fn(), }), })); vi.mock("@/hooks/use-lazy-load-messages", () => ({ useLazyLoadMessages: () => ({ loadMoreRaw: vi.fn(), hasMore: false }), })); vi.mock("@/components/task/chat/use-drain-older-messages", () => ({ useDrainOlderMessages: () => undefined, })); vi.mock("./chat/use-session-read-tracking", () => ({ useSessionReadTracking: () => null, })); vi.mock("@/lib/state/dockview-store", () => ({ useDockviewStore: (selector: (state: { scrollTarget: null }) => unknown) => selector({ scrollTarget: null }), getState: () => ({ scrollTarget: null }), })); vi.mock("@/hooks/domains/session/load-message-window", () => ({ loadMessageWindowAround: vi.fn(), })); vi.mock("@/lib/session-workspace-path", () => ({ getSessionWorkspacePath: () => undefined, })); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), })); import { TaskChatPanel } from "./task-chat-panel"; afterEach(() => { cleanup(); messageListRecoveryRevealKeys.length = 0; }); describe("TaskChatPanel launch-error ownership", () => { it("keeps a task-wide card and prior failed-session surfaces together", () => { render(); expect(screen.getByTestId("task-launch-error-entry").textContent).toContain( launchError.preview, ); expect(screen.getByTestId("session-stopped-banner")).toBeTruthy(); expect(screen.getByText(priorTranscriptMessage.content)).toBeTruthy(); expect(messageListRecoveryRevealKeys.at(-1)).toBe("prior-session:task-wide-launch-error"); }); it("reveals a persisted bootstrap fallback for the selected session", () => { const previousMetadata = panelState.session.metadata; panelState.session.metadata = { last_agent_error: { message: "The selected session could not start.", phase: "bootstrap", occurred_at: "2026-08-20T11:00:00Z", stamp: "persisted-bootstrap-error", }, }; try { render(); expect(messageListRecoveryRevealKeys.at(-1)).toBe("prior-session:persisted-bootstrap-error"); } finally { panelState.session.metadata = previousMetadata; } }); });