import { createElement } from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import type { AgentState } from "@/features/agents/state/store"; import { AgentChatPanel } from "@/features/agents/components/AgentChatPanel"; import type { GatewayModelChoice } from "@/lib/gateway/models"; import { formatThinkingMarkdown } from "@/lib/text/message-extract"; const createAgent = (): AgentState => ({ agentId: "agent-1", name: "Agent One", sessionKey: "agent:agent-1:studio:test-session", status: "idle", sessionCreated: true, awaitingUserInput: false, hasUnseenActivity: false, outputLines: [], lastResult: null, lastDiff: null, runId: null, runStartedAt: null, streamText: null, thinkingTrace: null, latestOverride: null, latestOverrideKind: null, lastAssistantMessageAt: null, lastActivityAt: null, latestPreview: null, lastUserMessage: null, draft: "", sessionSettingsSynced: true, historyLoadedAt: null, historyFetchLimit: null, historyFetchedCount: null, historyMaybeTruncated: false, toolCallingEnabled: true, showThinkingTraces: true, model: null, thinkingLevel: null, avatarSeed: "seed-1", avatarUrl: null, }); describe("AgentChatPanel controls", () => { const models: GatewayModelChoice[] = [ { provider: "openai", id: "gpt-5", name: "gpt-5", reasoning: true }, { provider: "openai", id: "gpt-5-mini", name: "gpt-5-mini", reasoning: false }, ]; afterEach(() => { cleanup(); }); it("renders_runtime_controls_in_agent_header_and_no_inline_name_editor", () => { render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onRename: vi.fn(async () => true), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(screen.getByLabelText("Model")).toBeInTheDocument(); expect(screen.getByLabelText("Thinking")).toBeInTheDocument(); expect(screen.queryByDisplayValue("Agent One")).not.toBeInTheDocument(); expect(screen.getByTestId("agent-rename-toggle")).toBeInTheDocument(); expect(screen.getByLabelText("Rename agent")).toBeInTheDocument(); expect(screen.getByTestId("agent-new-session-toggle")).toBeInTheDocument(); expect(screen.getByLabelText("Start new session")).toBeInTheDocument(); expect(screen.getByTestId("agent-settings-toggle")).toBeInTheDocument(); expect(screen.getByLabelText("Open behavior")).toBeInTheDocument(); expect(screen.queryByText("Inspect")).not.toBeInTheDocument(); }); it("renames_agent_inline_from_header", async () => { const onRename = vi.fn(async () => true); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onRename, onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); fireEvent.click(screen.getByTestId("agent-rename-toggle")); const input = screen.getByTestId("agent-rename-input") as HTMLInputElement; await waitFor(() => { expect(input).toHaveFocus(); expect(input.selectionStart).toBe(0); expect(input.selectionEnd).toBe("Agent One".length); }); fireEvent.change(input, { target: { value: " Agent Prime " } }); fireEvent.click(screen.getByTestId("agent-rename-save")); await waitFor(() => { expect(onRename).toHaveBeenCalledWith("Agent Prime"); }); }); it("cancels_inline_rename_without_saving", () => { const onRename = vi.fn(async () => true); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onRename, onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); fireEvent.click(screen.getByTestId("agent-rename-toggle")); fireEvent.change(screen.getByTestId("agent-rename-input"), { target: { value: "Edited Name" }, }); fireEvent.click(screen.getByTestId("agent-rename-cancel")); expect(onRename).not.toHaveBeenCalled(); expect(screen.queryByTestId("agent-rename-input")).not.toBeInTheDocument(); }); it("invokes_on_new_session_when_control_clicked", () => { const onNewSession = vi.fn(async () => {}); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onNewSession, onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); fireEvent.click(screen.getByTestId("agent-new-session-toggle")); expect(onNewSession).toHaveBeenCalledTimes(1); }); it("does_not_render_inline_status_badge_markers", () => { const { rerender, container } = render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const idleBadge = container.querySelector('[data-status="idle"]'); expect(idleBadge).toBeNull(); rerender( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running" }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const runningBadge = container.querySelector('[data-status="running"]'); expect(runningBadge).toBeNull(); }); it("invokes_on_model_change_when_model_select_changes_and_blurs_select", () => { const onModelChange = vi.fn(); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange, onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const modelSelect = screen.getByLabelText("Model") as HTMLSelectElement; modelSelect.focus(); expect(modelSelect).toHaveFocus(); fireEvent.change(modelSelect, { target: { value: "openai/gpt-5-mini" }, }); expect(onModelChange).toHaveBeenCalledWith("openai/gpt-5-mini"); expect(modelSelect).not.toHaveFocus(); }); it("invokes_on_thinking_change_when_thinking_select_changes", () => { const onThinkingChange = vi.fn(); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange, onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); fireEvent.change(screen.getByLabelText("Thinking"), { target: { value: "high" }, }); expect(onThinkingChange).toHaveBeenCalledWith("high"); }); it("invokes_on_open_settings_when_control_clicked", () => { const onOpenSettings = vi.fn(); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings, onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); fireEvent.click(screen.getByTestId("agent-settings-toggle")); expect(onOpenSettings).toHaveBeenCalledTimes(1); }); it("shows_stop_button_while_running_and_invokes_stop_handler", () => { const onStopRun = vi.fn(); render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running" }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun, onAvatarShuffle: vi.fn(), }) ); fireEvent.click(screen.getByRole("button", { name: "Stop" })); expect(onStopRun).toHaveBeenCalledTimes(1); }); it("allows_send_while_running_so_follow_up_can_be_queued", () => { const onSend = vi.fn(); render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running" }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const textarea = screen.getByPlaceholderText("type a message"); fireEvent.change(textarea, { target: { value: "follow up" } }); fireEvent.click(screen.getByRole("button", { name: "Send" })); expect(onSend).toHaveBeenCalledWith("follow up"); }); it("renders_queue_bar_and_supports_removing_queued_messages", () => { const onRemoveQueuedMessage = vi.fn(); render( createElement(AgentChatPanel, { agent: { ...createAgent(), queuedMessages: ["first queued", "second queued"] }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onRemoveQueuedMessage, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(screen.getByTestId("queued-messages-bar")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Remove queued message 1" })); expect(onRemoveQueuedMessage).toHaveBeenCalledWith(0); }); it("disables_stop_button_with_tooltip_when_stop_is_unavailable", () => { const stopDisabledReason = "This task is running as an automatic heartbeat check. Stopping heartbeat runs from Studio isn't available yet (coming soon)."; render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running" }, isSelected: true, canSend: true, models, stopBusy: false, stopDisabledReason, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const stopButton = screen.getByRole("button", { name: `Stop unavailable: ${stopDisabledReason}`, }); expect(stopButton).toBeDisabled(); expect(stopButton.parentElement).toHaveAttribute("title", stopDisabledReason); }); it("shows_thinking_indicator_while_running_before_stream_text", () => { render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running", outputLines: ["> test"] }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(screen.getByTestId("agent-typing-indicator")).toBeInTheDocument(); expect(within(screen.getByTestId("agent-typing-indicator")).getByText("Thinking")).toBeInTheDocument(); }); it("shows_thinking_indicator_after_stream_starts", () => { render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running", outputLines: ["> test"], streamText: "working on it", }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(screen.getByTestId("agent-typing-indicator")).toBeInTheDocument(); expect(within(screen.getByTestId("agent-typing-indicator")).getByText("Thinking")).toBeInTheDocument(); }); it("does_not_render_duplicate_typing_indicator_when_internal_thinking_is_visible", () => { render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running", outputLines: ["> test", formatThinkingMarkdown("thinking now")], }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(screen.queryByTestId("agent-typing-indicator")).not.toBeInTheDocument(); expect(screen.getByText("Thinking (internal)")).toBeInTheDocument(); }); it("renders thinking row collapsed by default", () => { render( createElement(AgentChatPanel, { agent: { ...createAgent(), status: "running", outputLines: ["> test", formatThinkingMarkdown("thinking now"), "final response"], }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend: vi.fn(), onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const details = screen.getByText("Thinking (internal)").closest("details"); expect(details).toBeTruthy(); expect(details).not.toHaveAttribute("open"); }); it("does_not_overwrite_active_draft_with_stale_nonempty_agent_draft", () => { const onDraftChange = vi.fn(); const onSend = vi.fn(); const { rerender } = render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange, onSend, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const textarea = screen.getByPlaceholderText("type a message") as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "hello world" } }); expect(textarea.value).toBe("hello world"); rerender( createElement(AgentChatPanel, { agent: { ...createAgent(), draft: "hello" }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange, onSend, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(textarea.value).toBe("hello world"); rerender( createElement(AgentChatPanel, { agent: { ...createAgent(), draft: "" }, isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange, onSend, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); expect(textarea.value).toBe(""); }); it("does_not_send_when_enter_is_pressed_during_composition", () => { const onSend = vi.fn(); render( createElement(AgentChatPanel, { agent: createAgent(), isSelected: true, canSend: true, models, stopBusy: false, onLoadMoreHistory: vi.fn(), onOpenSettings: vi.fn(), onModelChange: vi.fn(), onThinkingChange: vi.fn(), onDraftChange: vi.fn(), onSend, onStopRun: vi.fn(), onAvatarShuffle: vi.fn(), }) ); const textarea = screen.getByPlaceholderText("type a message"); fireEvent.change(textarea, { target: { value: "draft text" } }); fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", keyCode: 229, isComposing: true, }); expect(onSend).not.toHaveBeenCalled(); fireEvent.keyDown(textarea, { key: "Enter", code: "Enter" }); expect(onSend).toHaveBeenCalledWith("draft text"); }); });