Files
AgentHub/client/src/pages/__tests__/SessionDetail.nestedAgents.test.tsx
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

391 lines
13 KiB
TypeScript

/**
* @file SessionDetail.nestedAgents.test.tsx
* @description Tests for SessionDetail page focusing on correct rendering of nested agent hierarchies, including edge cases like orphaned subagents and multiple main agents. Validates expand/collapse behavior and descendant counts in the UI.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, within, waitFor } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { SessionDetail } from "../SessionDetail";
import type { Agent, Session, DashboardEvent } from "../../lib/types";
import { fmtCost } from "../../lib/format";
// ── Helpers ──────────────────────────────────────────────────────────────────
function makeAgent(overrides: Partial<Agent> = {}): Agent {
return {
id: "agent-1",
session_id: "sess-1",
name: "Main Agent",
type: "main",
subagent_type: null,
status: "working",
task: null,
current_tool: null,
started_at: "2026-03-05T10:00:00.000Z",
ended_at: null,
updated_at: "2026-03-05T10:00:00.000Z",
parent_agent_id: null,
metadata: null,
...overrides,
};
}
const mockSession: Session = {
id: "sess-1",
name: "Test Session",
status: "active",
cwd: "/test",
model: "claude-opus-4-6",
started_at: "2026-03-05T10:00:00.000Z",
ended_at: null,
metadata: null,
};
// ── Mock API ─────────────────────────────────────────────────────────────────
let mockAgents: Agent[] = [];
let mockCost: { total_cost: number; breakdown: unknown[] } = { total_cost: 0, breakdown: [] };
vi.mock("../../lib/api", () => ({
api: {
sessions: {
get: vi.fn(() =>
Promise.resolve({
session: mockSession,
agents: mockAgents,
events: [] as DashboardEvent[],
})
),
transcripts: vi.fn(() => Promise.resolve({ transcripts: [] })),
stats: vi.fn(() =>
Promise.resolve({
session_id: mockSession.id,
total_events: 0,
events_by_type: [],
tools_used: [],
error_count: 0,
first_event_at: null,
last_event_at: null,
agents: { total: 0, main: 0, subagent: 0, compaction: 0, by_status: {} },
subagent_types: [],
tokens: {
input_tokens: 0,
output_tokens: 0,
cache_read_tokens: 0,
cache_write_tokens: 0,
},
})
),
},
pricing: {
sessionCost: vi.fn(() => Promise.resolve(mockCost)),
},
events: {
list: vi.fn(() =>
Promise.resolve({
events: [] as DashboardEvent[],
limit: 50,
offset: 0,
total: 0,
})
),
facets: vi.fn(() => Promise.resolve({ event_types: [], tool_names: [] })),
},
},
}));
vi.mock("../../lib/eventBus", () => ({
eventBus: {
subscribe: vi.fn(() => () => {}),
},
}));
function renderPage() {
return render(
<MemoryRouter initialEntries={["/sessions/sess-1"]}>
<Routes>
<Route path="/sessions/:id" element={<SessionDetail />} />
</Routes>
</MemoryRouter>
);
}
/** Get the agent-tree region for scoped queries (avoids matches in the active-agent banner). */
async function findTree() {
return await screen.findByTestId("agent-tree");
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe("SessionDetail - Nested Agent Tree Rendering", () => {
beforeEach(() => {
mockAgents = [];
mockCost = { total_cost: 0, breakdown: [] };
});
it("shows the session total cost on the main agent card (cost is loaded separately)", async () => {
// Regression: /api/sessions/:id has no cost column, so the main card (which
// renders session.cost) showed nothing until SessionDetail injected the
// separately-loaded total into the card's session prop.
mockCost = { total_cost: 42.5, breakdown: [] };
mockAgents = [makeAgent({ id: "main-1", name: "Main Agent", type: "main", status: "waiting" })];
renderPage();
const tree = await findTree();
await waitFor(() => expect(within(tree).getByText(fmtCost(42.5))).toBeInTheDocument());
});
it("renders a flat main → subagent hierarchy (depth 1)", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main Agent", type: "main", status: "working" }),
makeAgent({
id: "sub-1",
name: "Explorer",
type: "subagent",
subagent_type: "Explore",
status: "working",
parent_agent_id: "main-1",
}),
];
renderPage();
expect(await screen.findByText("Main Agent")).toBeInTheDocument();
// Subagent should be visible (auto-expanded because it's working)
expect(await screen.findByText("Explorer")).toBeInTheDocument();
});
it("renders deeply nested agents (depth 3: main → L1 → L2 → L3)", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "waiting" }),
makeAgent({
id: "l1",
name: "Level-1",
type: "subagent",
status: "working",
parent_agent_id: "main-1",
}),
makeAgent({
id: "l2",
name: "Level-2",
type: "subagent",
status: "working",
parent_agent_id: "l1",
}),
makeAgent({
id: "l3",
name: "Level-3",
type: "subagent",
status: "working",
parent_agent_id: "l2",
}),
];
renderPage();
const tree = await findTree();
// All levels should render in the tree (auto-expanded because they have working children).
// The active-agent banner may also display "Level-1", so we scope to the tree.
await waitFor(() => expect(within(tree).getByText("Main")).toBeInTheDocument());
expect(within(tree).getByText("Level-1")).toBeInTheDocument();
expect(within(tree).getByText("Level-2")).toBeInTheDocument();
expect(within(tree).getByText("Level-3")).toBeInTheDocument();
});
it("shows descendant count in collapsed badge for nested agents", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "waiting" }),
makeAgent({
id: "l1",
name: "Level-1",
type: "subagent",
status: "completed",
parent_agent_id: "main-1",
}),
makeAgent({
id: "l2",
name: "Level-2",
type: "subagent",
status: "completed",
parent_agent_id: "l1",
}),
makeAgent({
id: "l3a",
name: "Level-3a",
type: "subagent",
status: "completed",
parent_agent_id: "l2",
}),
makeAgent({
id: "l3b",
name: "Level-3b",
type: "subagent",
status: "completed",
parent_agent_id: "l2",
}),
];
renderPage();
// Main has 4 total descendants (L1, L2, L3a, L3b) - should show "4 subagents" when collapsed
expect(await screen.findByText("4 subagents")).toBeInTheDocument();
});
it("expands and collapses nested agent groups", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "waiting" }),
makeAgent({
id: "l1",
name: "Level-1",
type: "subagent",
status: "completed",
parent_agent_id: "main-1",
}),
makeAgent({
id: "l2",
name: "Level-2",
type: "subagent",
status: "completed",
parent_agent_id: "l1",
}),
];
renderPage();
// Initially collapsed (agents are completed, no auto-expand)
expect(await screen.findByText("2 subagents")).toBeInTheDocument();
expect(screen.queryByText("Level-1")).not.toBeInTheDocument();
// Click the count badge to expand
fireEvent.click(screen.getByText("2 subagents"));
expect(await screen.findByText("Level-1")).toBeInTheDocument();
// Level-2 is still nested under Level-1 which is collapsed
expect(screen.getByText("1 subagent")).toBeInTheDocument();
});
it("renders orphaned subagents in dedicated section", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "waiting" }),
makeAgent({
id: "orphan-1",
name: "Orphan Agent",
type: "subagent",
status: "working",
parent_agent_id: "nonexistent-parent",
}),
];
renderPage();
const tree = await findTree();
expect(within(tree).getByText("Main")).toBeInTheDocument();
expect(within(tree).getByText("Orphan Agent")).toBeInTheDocument();
expect(await screen.findByText("Unparented Subagents")).toBeInTheDocument();
});
it("auto-expands all ancestors when a deeply nested agent is active", async () => {
// Level-3 is working → Level-2, Level-1, and Main should all auto-expand
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "waiting" }),
makeAgent({
id: "l1",
name: "Level-1",
type: "subagent",
status: "working",
parent_agent_id: "main-1",
}),
makeAgent({
id: "l2",
name: "Level-2",
type: "subagent",
status: "working",
parent_agent_id: "l1",
}),
makeAgent({
id: "l3",
name: "Deep Active",
type: "subagent",
status: "working",
parent_agent_id: "l2",
}),
];
renderPage();
const tree = await findTree();
// All levels should be visible because l3 is working, triggering ancestor expansion
await waitFor(() => expect(within(tree).getByText("Main")).toBeInTheDocument());
expect(within(tree).getByText("Level-1")).toBeInTheDocument();
expect(within(tree).getByText("Level-2")).toBeInTheDocument();
expect(within(tree).getByText("Deep Active")).toBeInTheDocument();
});
it("handles agents with no children (leaf nodes)", async () => {
mockAgents = [makeAgent({ id: "main-1", name: "Main", type: "main", status: "working" })];
renderPage();
expect(await screen.findByText("Main")).toBeInTheDocument();
// No subagent-count button should exist for a leaf node. Match the
// "{{count}} subagent(s)" label specifically so we don't collide with
// the word "subagent" in unrelated explanatory copy elsewhere on the page.
expect(screen.queryByText(/\d+ subagent/)).not.toBeInTheDocument();
});
it("renders multiple main agents in the same session", async () => {
// Edge case: import/resume could create multiple "main" entries
mockAgents = [
makeAgent({ id: "main-1", name: "Main-A", type: "main", status: "completed" }),
makeAgent({ id: "main-2", name: "Main-B", type: "main", status: "working" }),
makeAgent({
id: "sub-a",
name: "Sub of A",
type: "subagent",
status: "completed",
parent_agent_id: "main-1",
}),
makeAgent({
id: "sub-b",
name: "Sub of B",
type: "subagent",
status: "working",
parent_agent_id: "main-2",
}),
];
renderPage();
const tree = await findTree();
await waitFor(() => expect(within(tree).getByText("Main-A")).toBeInTheDocument());
expect(within(tree).getByText("Main-B")).toBeInTheDocument();
// Sub of B auto-expanded (working)
expect(within(tree).getByText("Sub of B")).toBeInTheDocument();
});
it("renders sibling subagents at the same depth", async () => {
mockAgents = [
makeAgent({ id: "main-1", name: "Main", type: "main", status: "working" }),
makeAgent({
id: "sub-a",
name: "Sibling-A",
type: "subagent",
status: "working",
parent_agent_id: "main-1",
}),
makeAgent({
id: "sub-b",
name: "Sibling-B",
type: "subagent",
status: "working",
parent_agent_id: "main-1",
}),
makeAgent({
id: "sub-c",
name: "Sibling-C",
type: "subagent",
status: "completed",
parent_agent_id: "main-1",
}),
];
renderPage();
expect(await screen.findByText("Main")).toBeInTheDocument();
expect(await screen.findByText("Sibling-A")).toBeInTheDocument();
expect(await screen.findByText("Sibling-B")).toBeInTheDocument();
expect(await screen.findByText("Sibling-C")).toBeInTheDocument();
});
});