feat(web): add agent output terminal tabs for orchestrator sessions (#522)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #522.
This commit is contained in:
@@ -33,6 +33,20 @@ vi.mock("./XTerminal", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock AgentTerminal to avoid complexity in panel tests
|
||||
vi.mock("./AgentTerminal", () => ({
|
||||
AgentTerminal: vi.fn(
|
||||
({ agent }: { agent: { agentId: string; agentType: string; status: string } }) => (
|
||||
<div
|
||||
data-testid="mock-agent-terminal"
|
||||
data-agent-id={agent.agentId}
|
||||
data-agent-type={agent.agentType}
|
||||
data-status={agent.status}
|
||||
/>
|
||||
)
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock useTerminalSessions
|
||||
const mockCreateSession = vi.fn();
|
||||
const mockCloseSession = vi.fn();
|
||||
@@ -72,6 +86,29 @@ vi.mock("@/hooks/useTerminalSessions", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock useAgentStream
|
||||
const mockDismissAgent = vi.fn();
|
||||
let mockAgents = new Map<
|
||||
string,
|
||||
{
|
||||
agentId: string;
|
||||
agentType: string;
|
||||
status: "spawning" | "running" | "completed" | "error";
|
||||
outputLines: string[];
|
||||
startedAt: number;
|
||||
}
|
||||
>();
|
||||
let mockAgentStreamConnected = false;
|
||||
|
||||
vi.mock("@/hooks/useAgentStream", () => ({
|
||||
useAgentStream: vi.fn(() => ({
|
||||
agents: mockAgents,
|
||||
isConnected: mockAgentStreamConnected,
|
||||
connectionError: null,
|
||||
dismissAgent: mockDismissAgent,
|
||||
})),
|
||||
}));
|
||||
|
||||
import { TerminalPanel } from "./TerminalPanel";
|
||||
|
||||
// ==========================================
|
||||
@@ -100,6 +137,8 @@ describe("TerminalPanel", () => {
|
||||
mockIsConnected = false;
|
||||
mockConnectionError = null;
|
||||
mockRegisterOutputCallback.mockReturnValue(vi.fn());
|
||||
mockAgents = new Map();
|
||||
mockAgentStreamConnected = false;
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
@@ -425,4 +464,118 @@ describe("TerminalPanel", () => {
|
||||
expect(mockCreateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Agent tab integration
|
||||
// ==========================================
|
||||
|
||||
describe("agent tab integration", () => {
|
||||
function setOneAgent(status: "spawning" | "running" | "completed" | "error" = "running"): void {
|
||||
mockAgents = new Map([
|
||||
[
|
||||
"agent-1",
|
||||
{
|
||||
agentId: "agent-1",
|
||||
agentType: "worker",
|
||||
status,
|
||||
outputLines: ["Hello from agent\n"],
|
||||
startedAt: Date.now() - 3000,
|
||||
},
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
it("renders an agent tab when an agent is active", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getAllByTestId("agent-tab")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders no agent tabs when agents map is empty", () => {
|
||||
mockAgents = new Map();
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.queryByTestId("agent-tab")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("agent tab button has the agent type as label", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("tab", { name: "Agent: worker" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("agent tab has role=tab", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("tab", { name: "Agent: worker" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows dismiss button for completed agents", () => {
|
||||
setOneAgent("completed");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("button", { name: "Dismiss worker agent" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows dismiss button for error agents", () => {
|
||||
setOneAgent("error");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("button", { name: "Dismiss worker agent" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show dismiss button for running agents", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Dismiss worker agent" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show dismiss button for spawning agents", () => {
|
||||
setOneAgent("spawning");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Dismiss worker agent" })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls dismissAgent when dismiss button is clicked", () => {
|
||||
setOneAgent("completed");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Dismiss worker agent" }));
|
||||
expect(mockDismissAgent).toHaveBeenCalledWith("agent-1");
|
||||
});
|
||||
|
||||
it("renders AgentTerminal when agent tab is active", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
// Click the agent tab to make it active
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Agent: worker" }));
|
||||
// AgentTerminal should be rendered (mock shows mock-agent-terminal)
|
||||
expect(screen.getByTestId("mock-agent-terminal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a divider between terminal and agent tabs", () => {
|
||||
setTwoSessions();
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
// The divider div is aria-hidden; check it's present in the DOM
|
||||
const tablist = screen.getByRole("tablist");
|
||||
const divider = tablist.querySelector('[aria-hidden="true"][style*="width: 1"]');
|
||||
expect(divider).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("agent tabs show correct data-agent-status", () => {
|
||||
setOneAgent("running");
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
const tab = screen.getByTestId("agent-tab");
|
||||
expect(tab).toHaveAttribute("data-agent-status", "running");
|
||||
});
|
||||
|
||||
it("empty state not shown when agents exist but no terminal sessions", () => {
|
||||
mockSessions = new Map();
|
||||
setOneAgent("running");
|
||||
mockIsConnected = false;
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.queryByText("Connecting...")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user