Files
fusion/packages/dashboard/app/components/__tests__/ActiveAgentsPanel.test.tsx
2026-04-13 15:35:08 -07:00

291 lines
8.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { ActiveAgentsPanel } from "../ActiveAgentsPanel";
import type { Agent } from "../../api";
import { useLiveTranscript } from "../../hooks/useLiveTranscript";
// Mock useLiveTranscript
vi.mock("../../hooks/useLiveTranscript", () => ({
useLiveTranscript: vi.fn().mockReturnValue({
entries: [],
isConnected: false,
}),
}));
const mockUseLiveTranscript = vi.mocked(useLiveTranscript);
describe("ActiveAgentsPanel", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: false,
});
});
it("renders live transcript text from entries", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [
{ type: "text", text: "Processing request...", timestamp: "2026-01-01T00:01:00Z" },
{ type: "text", text: "Analyzing code...", timestamp: "2026-01-01T00:02:00Z" },
],
isConnected: true,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
expect(screen.getByText("Processing request...")).toBeInTheDocument();
expect(screen.getByText("Analyzing code...")).toBeInTheDocument();
});
it("passes projectId from props to useLiveTranscript hook", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} projectId="my-project" />);
// Verify the hook was called with the projectId
expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", "my-project");
});
it("passes undefined projectId when not provided", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
// Verify the hook was called without projectId
expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", undefined);
});
it("renders empty state when no entries yet", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: false,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
expect(screen.getByText("Connecting...")).toBeInTheDocument();
});
it("renders 'Waiting for output...' when connected but no entries", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: true,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
expect(screen.getByText("Waiting for output...")).toBeInTheDocument();
});
it("renders multiple agent cards with separate transcript streams", async () => {
mockUseLiveTranscript
.mockReturnValueOnce({
entries: [{ type: "text", text: "Agent 1 output", timestamp: "2026-01-01T00:01:00Z" }],
isConnected: true,
})
.mockReturnValueOnce({
entries: [{ type: "text", text: "Agent 2 output", timestamp: "2026-01-01T00:02:00Z" }],
isConnected: true,
});
const mockAgent1: Agent = {
id: "agent-001",
name: "Agent One",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
const mockAgent2: Agent = {
id: "agent-002",
name: "Agent Two",
role: "reviewer",
state: "running",
taskId: "FN-002",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent1, mockAgent2]} />);
expect(screen.getByText("Agent 1 output")).toBeInTheDocument();
expect(screen.getByText("Agent 2 output")).toBeInTheDocument();
});
it("renders up to 20 transcript lines per card", async () => {
// The component receives entries and slices to first 20
// In real usage, the hook prepends new entries, so most recent first
// For the mock, we simulate this by providing entries in reverse order
const manyEntries = Array.from({ length: 25 }, (_, i) => ({
type: "text" as const,
text: `Line ${24 - i}`, // Reversed: 24, 23, 22, ..., 1, 0
timestamp: new Date().toISOString(),
}));
mockUseLiveTranscript.mockReturnValue({
entries: manyEntries,
isConnected: true,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
// Should show the first 20 entries (most recent first)
// With reversed entries, slice(0, 20) gives us Line 24 through Line 5
expect(screen.getByText("Line 24")).toBeInTheDocument();
expect(screen.queryByText("Line 4")).not.toBeInTheDocument(); // Line 4 is beyond index 20
});
it("returns null when agents array is empty", async () => {
const { container } = render(<ActiveAgentsPanel agents={[]} />);
expect(container.firstChild).toBeNull();
});
it("displays agent name and task badge", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: false,
});
const mockAgent: Agent = {
id: "agent-001",
name: "My Agent",
role: "executor",
state: "running",
taskId: "FN-042",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent]} />);
expect(screen.getByText("My Agent")).toBeInTheDocument();
expect(screen.getByText("FN-042")).toBeInTheDocument();
});
it("calls onAgentSelect with agent ID when card is clicked", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: false,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
const handleSelect = vi.fn();
render(<ActiveAgentsPanel agents={[mockAgent]} onAgentSelect={handleSelect} />);
fireEvent.click(screen.getByRole("button", { name: /select agent test agent/i }));
expect(handleSelect).toHaveBeenCalledWith("agent-001");
});
it("shows active indicator when connected", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [{ type: "text", text: "Test", timestamp: "2026-01-01T00:00:00Z" }],
isConnected: true,
});
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
const { container } = render(<ActiveAgentsPanel agents={[mockAgent]} />);
// The streaming dot should be present when connected
const streamingDot = container.querySelector(".live-agent-streaming-dot");
expect(streamingDot).toBeInTheDocument();
});
it("passes projectId through to hook for each agent card", async () => {
mockUseLiveTranscript.mockReturnValue({
entries: [],
isConnected: false,
});
const mockAgent1: Agent = {
id: "agent-001",
name: "Agent One",
role: "executor",
state: "running",
taskId: "FN-001",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
const mockAgent2: Agent = {
id: "agent-002",
name: "Agent Two",
role: "executor",
state: "running",
taskId: "FN-002",
lastHeartbeatAt: new Date().toISOString(),
} as Agent;
render(<ActiveAgentsPanel agents={[mockAgent1, mockAgent2]} projectId="shared-project" />);
// Both agents should receive the same projectId
expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-001", "shared-project");
expect(mockUseLiveTranscript).toHaveBeenCalledWith("FN-002", "shared-project");
});
});