import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { DIProvider } from "@/app/di"; import type { AgentProfile } from "@/domain"; import type { Gateways } from "@/ports"; import { CustomAgentChatView } from "./CustomAgentChatView"; const profile: AgentProfile = { id: "structured", name: "Structured Codex", command: "codex", args: [], contextInjection: { strategy: "conventionFile", target: "AGENTS.md" }, detect: null, cwdTemplate: "{projectRoot}", structuredAdapter: "codex", }; describe("CustomAgentChatView", () => { it("cancels only the current turn and keeps the structured session alive", async () => { const agent = { launchAgentChat: vi.fn(), reattachAgentChat: vi.fn(async (sessionId: string) => ({ sessionId, scrollback: [], })), sendAgentChat: vi.fn(() => new Promise(() => {})), cancelAgentChat: vi.fn(async () => {}), closeAgentChat: vi.fn(async () => {}), }; const onSessionId = vi.fn(); render( null) }, } as unknown as Gateways} > , ); await waitFor(() => expect(agent.reattachAgentChat).toHaveBeenCalledWith( "chat-session-1", expect.any(Function), ), ); fireEvent.change(screen.getByLabelText(/message CLI custom/), { target: { value: "first turn" }, }); fireEvent.click(screen.getByRole("button", { name: "Envoyer" })); await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledWith( "chat-session-1", "first turn", expect.any(Function), ), ); fireEvent.click(await screen.findByRole("button", { name: "Cancel" })); await waitFor(() => expect(agent.cancelAgentChat).toHaveBeenCalledWith("chat-session-1"), ); expect(agent.closeAgentChat).not.toHaveBeenCalled(); expect(onSessionId).not.toHaveBeenCalledWith(null); expect(screen.getByText("Tour interrompu.")).toBeTruthy(); fireEvent.change(screen.getByLabelText(/message CLI custom/), { target: { value: "second turn" }, }); fireEvent.click(screen.getByRole("button", { name: "Envoyer" })); await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(2)); expect(agent.sendAgentChat).toHaveBeenLastCalledWith( "chat-session-1", "second turn", expect.any(Function), ); }); it("falls back to a fresh launch when reattach reports NOT_FOUND", async () => { const agent = { launchAgentChat: vi.fn(async () => ({ sessionId: "chat-session-2", assignedConversationId: "conversation-2", })), reattachAgentChat: vi .fn() .mockRejectedValueOnce({ code: "NOT_FOUND", message: "structured session gone" }) .mockImplementationOnce(async (sessionId: string) => ({ sessionId, scrollback: [], })), sendAgentChat: vi.fn(() => new Promise(() => {})), cancelAgentChat: vi.fn(async () => {}), closeAgentChat: vi.fn(async () => {}), }; const onSessionId = vi.fn(); render( null) }, } as unknown as Gateways} > , ); await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledWith("project-1", "agent-1", { cwd: "/repo", rows: 24, cols: 80, conversationId: "conversation-1", nodeId: "node-1", }), ); await waitFor(() => expect(onSessionId).toHaveBeenCalledWith("chat-session-2"), ); expect(screen.queryByText(/structured session gone/)).toBeNull(); }); it("surfaces non-NOT_FOUND reattach errors instead of launching a new session", async () => { const agent = { launchAgentChat: vi.fn(), reattachAgentChat: vi.fn().mockRejectedValueOnce({ code: "PROCESS", message: "structured session failed to attach", }), sendAgentChat: vi.fn(() => new Promise(() => {})), cancelAgentChat: vi.fn(async () => {}), closeAgentChat: vi.fn(async () => {}), }; render( null) }, } as unknown as Gateways} > , ); expect((await screen.findByRole("alert")).textContent).toContain( "structured session failed to attach", ); expect(agent.launchAgentChat).not.toHaveBeenCalled(); }); });