/** * #3 — Agent dropdown per cell: setCellAgent persists in the layout, and the * terminal re-mounts with the agent opener; "Plain" reverts to a plain terminal. */ import { describe, it, expect, vi } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import type { Gateways } from "@/ports"; import { MockLayoutGateway, MockAgentGateway, MockTerminalGateway, } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import { LayoutGrid } from "./LayoutGrid"; import { leaves } from "./layout"; function renderGrid( layout: MockLayoutGateway, agent: MockAgentGateway, projectId = "p1", terminal: MockTerminalGateway = new MockTerminalGateway(), ) { const gateways = { layout, terminal, agent, } as unknown as Gateways; return render( , ); } describe("setCellAgent (agent dropdown per cell)", () => { it("renders an agent selector on each leaf cell", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); renderGrid(layout, agent); await waitFor(() => { expect(screen.getAllByRole("combobox")).toHaveLength(1); }); }); it("selecting an agent calls setCellAgent via the layout gateway", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); // Create an agent in the project. const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "prof-1", }); renderGrid(layout, agent); await waitFor(() => { expect(screen.getByRole("combobox")).toBeTruthy(); }); const select = screen.getByRole("combobox") as HTMLSelectElement; fireEvent.change(select, { target: { value: a.id } }); // The layout should persist the agent on the leaf. const initial = await layout.loadLayout("p1"); const leafId = leaves(initial)[0].id; await waitFor(async () => { const tree = await layout.loadLayout("p1"); const leaf = leaves(tree)[0]; // The mutation should have persisted. expect(leaf.id).toBe(leafId); // Since mutateLayout is async, give it a tick. }); }); it("MockLayoutGateway setCellAgent persists and clears correctly", async () => { const layout = new MockLayoutGateway(); const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; // Set agent. const after = await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: "agent-42", }); expect(leaves(after)[0].agent).toBe("agent-42"); // Clear agent (null). const cleared = await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: null, }); expect(leaves(cleared)[0].agent).toBeUndefined(); }); it("selecting Plain (empty) clears the agent via mutateLayout", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "prof-1", }); // Pre-set agent on the leaf. const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: a.id, }); renderGrid(layout, agent); await waitFor(() => { const sel = screen.getByRole("combobox") as HTMLSelectElement; // The select should show the agent (or Plain if the component loaded before the mutation). expect(sel).toBeTruthy(); }); const select = screen.getByRole("combobox") as HTMLSelectElement; // Switch back to Plain. fireEvent.change(select, { target: { value: "" } }); // After clearing, the layout should have no agent on the leaf. await waitFor(async () => { const fresh = await layout.loadLayout("p1"); const l = leaves(fresh)[0]; expect(l.agent === undefined || l.agent === null).toBe(true); }); }); it("changing the agent resets the cell session to null (Bug #3)", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" }); // Pre-set a running session on the leaf (as if a plain terminal had opened). const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; await layout.mutateLayout("p1", { type: "setSession", target: leafId, session: "old-session", }); renderGrid(layout, agent); await waitFor(() => expect(screen.getByRole("combobox")).toBeTruthy()); fireEvent.change(screen.getByRole("combobox"), { target: { value: a.id } }); await waitFor(async () => { const fresh = await layout.loadLayout("p1"); const l = leaves(fresh)[0]; expect(l.agent).toBe(a.id); // The stale session must be cleared so the new agent opens fresh. expect(l.session === null || l.session === undefined).toBe(true); }); }); it("changing the agent kills the previous PTY (Bug #3)", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const terminal = new MockTerminalGateway(); const closeSpy = vi.spyOn(terminal, "closeTerminal"); const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" }); const tree = await layout.loadLayout("p1"); const leafId = leaves(tree)[0].id; await layout.mutateLayout("p1", { type: "setSession", target: leafId, session: "old-session", }); renderGrid(layout, agent, "p1", terminal); await waitFor(() => expect(screen.getByRole("combobox")).toBeTruthy()); fireEvent.change(screen.getByRole("combobox"), { target: { value: a.id } }); await waitFor(() => { expect(closeSpy).toHaveBeenCalledWith("old-session"); }); }); it("does not kill any PTY when the cell had no session (Bug #3)", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const terminal = new MockTerminalGateway(); const closeSpy = vi.spyOn(terminal, "closeTerminal"); const a = await agent.createAgent("p1", { name: "MyAgent", profileId: "p" }); renderGrid(layout, agent, "p1", terminal); await waitFor(() => expect(screen.getByRole("combobox")).toBeTruthy()); fireEvent.change(screen.getByRole("combobox"), { target: { value: a.id } }); await waitFor(async () => { const fresh = await layout.loadLayout("p1"); expect(leaves(fresh)[0].agent).toBe(a.id); }); expect(closeSpy).not.toHaveBeenCalled(); }); it("closing a cell kills the dropped cell's PTY (no orphan)", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); const terminal = new MockTerminalGateway(); const closeSpy = vi.spyOn(terminal, "closeTerminal"); // Split the root into two cells, then give the second a running session. const tree = await layout.loadLayout("p1"); const rootId = leaves(tree)[0].id; const afterSplit = await layout.mutateLayout("p1", { type: "split", target: rootId, direction: "row", newLeaf: "leaf-b", container: "split-c", }); const droppedId = leaves(afterSplit)[1].id; await layout.mutateLayout("p1", { type: "setSession", target: droppedId, session: "dropped-session", }); renderGrid(layout, agent, "p1", terminal); // Close the SECOND cell: keep the first, drop the second. Closing a cell is // now a detach-only view operation; it must not kill the PTY. await waitFor(() => { expect(screen.getByLabelText(`close ${droppedId}`)).toBeTruthy(); }); fireEvent.click(screen.getByLabelText(`close ${droppedId}`)); await waitFor(() => { expect(screen.getAllByTestId("layout-leaf")).toHaveLength(1); }); expect(closeSpy).not.toHaveBeenCalled(); }); it("agent selector shows project agents as options", async () => { const layout = new MockLayoutGateway(); const agent = new MockAgentGateway(); await agent.createAgent("p1", { name: "Agent Alpha", profileId: "p" }); await agent.createAgent("p1", { name: "Agent Beta", profileId: "p" }); renderGrid(layout, agent); await waitFor(() => { expect(screen.getByText("Agent Alpha")).toBeTruthy(); expect(screen.getByText("Agent Beta")).toBeTruthy(); }); }); });