/** * F-1 — `LayoutGrid` cell routing (Option 1, Terminal + MCP): **every** agent * cell renders the raw {@link TerminalView}; no structured chat view is ever * mounted. This replaces the former §17.6 `cellKind:"chat"` routing — the human * view is now the native interactive PTY, and cross-model delegation flows * through MCP tools, not a chat view. Wired through the real {@link DIProvider} * with the in-memory mocks, exactly like `LayoutGrid.test.tsx`. * * The decisive case: an agent cell always renders the terminal and never swaps * to a chat view (the structured chat surface was removed in the F-2 cleanup). * * Under jsdom xterm's `open` may bail, so the opener that triggers the launch * might not run; we therefore stub xterm (as in the original test) so the launch * does fire and we genuinely exercise the post-launch routing — which must stay * on the terminal regardless of the reported kind. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, waitFor as rtlWaitFor } from "@testing-library/react"; // Make xterm "wire up" under jsdom: the real `Terminal.open` throws without a // layout engine, which makes `TerminalView`'s effect bail before it ever calls // the opener — so the launch would never fire. A minimal stub lets `term.open` // succeed and the opener run, so the launch (and any routing it could trigger) // is genuinely exercised. We do NOT stub the routing — only xterm. vi.mock("@xterm/xterm", () => ({ Terminal: class { loadAddon() {} open() {} onData() { return { dispose() {} }; } onResize() { return { dispose() {} }; } write() {} dispose() {} get cols() { return 80; } get rows() { return 24; } }, })); vi.mock("@xterm/addon-fit", () => ({ FitAddon: class { fit() {} }, })); vi.mock("@xterm/xterm/css/xterm.css", () => ({})); // jsdom has no ResizeObserver; TerminalView installs one after `term.open`. if (typeof globalThis.ResizeObserver === "undefined") { globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver; } import type { Gateways } from "@/ports"; import { MockAgentGateway, MockLayoutGateway, MockSystemGateway, MockTerminalGateway } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import { leaves } from "./layout"; import { LayoutGrid } from "./LayoutGrid"; /** Seeds an agent in the gateway and pins it onto the (single) leaf cell. */ async function seedPinnedAgent(): Promise<{ gateways: Gateways; layout: MockLayoutGateway; agentGateway: MockAgentGateway; }> { const layout = new MockLayoutGateway(); const agentGateway = new MockAgentGateway(); const terminal = new MockTerminalGateway(); const system = new MockSystemGateway(); const agent = await agentGateway.createAgent("p1", { name: "Worker", profileId: "claude" }); // Pin the agent onto the single leaf. const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: agent.id }); const gateways = { layout, agent: agentGateway, terminal, system } as unknown as Gateways; return { gateways, layout, agentGateway }; } function renderGrid(gateways: Gateways) { return render( , ); } describe("LayoutGrid cell routing (F-1, Terminal + MCP)", () => { it("a plain (agent-less) cell renders the terminal view, never a chat view", async () => { const layout = new MockLayoutGateway(); const gateways = { layout, agent: new MockAgentGateway(), terminal: new MockTerminalGateway(), system: new MockSystemGateway(), } as unknown as Gateways; renderGrid(gateways); await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy()); expect(screen.getByTestId("terminal-view")).toBeTruthy(); expect(screen.queryByTestId("agent-chat-view")).toBeNull(); }); it("a pty agent cell renders the terminal view, never a chat view", async () => { const { gateways } = await seedPinnedAgent(); renderGrid(gateways); await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy()); expect(screen.getByTestId("terminal-view")).toBeTruthy(); expect(screen.queryByTestId("agent-chat-view")).toBeNull(); }); it("re-mounting a known agent cell (persisted session) repaints as a terminal, never chat", async () => { const layout = new MockLayoutGateway(); const agentGateway = new MockAgentGateway(); const terminal = new MockTerminalGateway(); const system = new MockSystemGateway(); const agent = await agentGateway.createAgent("p1", { name: "Worker", profileId: "claude" }); // Seed a persisted session on the leaf — the pre-F-1 path would have re-mounted // such a known agent cell as a chat view; now it must always be a terminal. const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: agent.id }); await layout.mutateLayout("p1", { type: "setSession", target: leafId, session: "running-session", }); const gateways = { layout, agent: agentGateway, terminal, system } as unknown as Gateways; // First mount. const { unmount } = renderGrid(gateways); await rtlWaitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy()); expect(screen.queryByTestId("agent-chat-view")).toBeNull(); unmount(); // Re-mount (as after a tab/layout navigation): the known agent cell with its // persisted session must repaint as a terminal — never a chat view. renderGrid(gateways); await rtlWaitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy()); expect(screen.queryByTestId("agent-chat-view")).toBeNull(); }); });