import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, fireEvent, act, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { loadAllAppCss } from "../../test/cssFixture"; import type { AgentHeartbeatRun } from "../../api"; import type { AgentLogEntry } from "@fusion/core"; import { DEFAULT_HEARTBEAT_INTERVAL_MS } from "../../utils/heartbeatIntervals"; import { MOCK_SKILLS, createMockAgent, mockConfirm, mockDeleteAgent, mockFetchAgent, mockFetchAgentBudgetStatus, mockFetchAgentChildren, mockFetchAgentLogsWithMeta, mockFetchAgentMailbox, mockFetchAgentMemoryFile, mockFetchAgentMemoryFiles, mockFetchAgentRunDetail, mockFetchAgentRunLogs, mockFetchAgentPromptSizes, mockFetchAgentRuns, mockFetchAgentTasks, mockFetchAgents, mockFetchChainOfCommand, mockFetchCompanies, mockFetchDiscoveredSkills, mockFetchModels, mockFetchPluginRuntimes, mockFetchSkillContent, mockFetchWorkspaceFileContent, mockMarkMessageRead, mockResetAgentBudget, mockSaveAgentMemoryFile, mockSaveWorkspaceFileContent, mockStartAgentRun, mockSubscribeSse, mockUpdateAgent, mockUpdateAgentInstructions, mockUpdateAgentMemory, mockUpdateAgentSoul, mockUpdateAgentState, mockUpdateGlobalSettings, mockUpgradeAgentHeartbeatProcedure, setupAgentDetailMocks, } from "./AgentDetailView.test-helpers"; import { AgentDetailView } from "../AgentDetailView"; describe("AgentDetailView — logs, tasks, and runs", () => { beforeEach(() => { setupAgentDetailMocks(); vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.includes("/token-usage")) { return new Response(JSON.stringify({ last24h: { totalInputTokens: 100, totalCachedTokens: 50, totalCacheWriteTokens: 10, totalOutputTokens: 20, nTasks: 2, hitRatio: 0.3333 }, last7d: { totalInputTokens: 200, totalCachedTokens: 100, totalCacheWriteTokens: 20, totalOutputTokens: 40, nTasks: 3, hitRatio: 0.3333 }, allTime: { totalInputTokens: 300, totalCachedTokens: 150, totalCacheWriteTokens: 30, totalOutputTokens: 60, nTasks: 4, hitRatio: 0.3333 }, }), { status: 200, headers: { "Content-Type": "application/json" } }); } return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }); })); }); describe("Logs tab", () => { it("loads latest run logs lazily for agents without a current task", async () => { const latestRun = { id: "run-1001", agentId: "agent-001", startedAt: "2024-01-01T00:00:00.000Z", endedAt: null, status: "active", } as AgentHeartbeatRun; mockFetchAgent.mockResolvedValue(createMockAgent({ taskId: undefined, activeRun: latestRun, completedRuns: [], })); mockFetchAgentRuns.mockResolvedValue([latestRun]); mockFetchAgentRunLogs.mockResolvedValue([ { timestamp: "2024-01-01T00:01:00.000Z", taskId: "agent-run", text: "First entry", type: "text" }, { timestamp: "2024-01-01T00:02:00.000Z", taskId: "agent-run", text: "Second entry", type: "text" }, ]); render( ); await waitFor(() => { expect(screen.getByText("Dashboard")).toBeInTheDocument(); }); expect(mockFetchAgentRuns).not.toHaveBeenCalled(); expect(mockFetchAgentRunLogs).not.toHaveBeenCalled(); fireEvent.click(screen.getByText("Logs")); await waitFor(() => { expect(mockFetchAgentRuns).toHaveBeenCalledWith("agent-001", 1, undefined); expect(mockFetchAgentRunLogs).toHaveBeenCalledWith("agent-001", "run-1001", undefined); }); expect(screen.getByText("Latest run · run-1001")).toBeInTheDocument(); await waitFor(() => { const viewer = screen.getByTestId("agent-log-viewer"); expect(viewer.textContent).toContain("First entry"); expect(viewer.textContent).toContain("Second entry"); }); }); it("renders log entries in chronological order (oldest first)", async () => { const latestRun = { id: "run-1002", agentId: "agent-001", startedAt: "2024-01-01T00:00:00.000Z", endedAt: null, status: "active", } as AgentHeartbeatRun; mockFetchAgent.mockResolvedValue(createMockAgent({ taskId: undefined, activeRun: latestRun, completedRuns: [], })); mockFetchAgentRuns.mockResolvedValue([latestRun]); mockFetchAgentRunLogs.mockResolvedValue([ { timestamp: "2024-01-01T00:01:00.000Z", taskId: "agent-run", text: "Oldest entry", type: "text" }, { timestamp: "2024-01-01T00:02:00.000Z", taskId: "agent-run", text: "Middle entry", type: "text" }, { timestamp: "2024-01-01T00:03:00.000Z", taskId: "agent-run", text: "Newest entry", type: "text" }, ]); render( ); await waitFor(() => { expect(screen.getByText("Dashboard")).toBeInTheDocument(); }); fireEvent.click(screen.getByText("Logs")); await waitFor(() => { expect(screen.getByText("Oldest entry")).toBeInTheDocument(); }); const viewerText = screen.getByTestId("agent-log-viewer").textContent ?? ""; expect(viewerText.indexOf("Oldest entry")).toBeLessThan(viewerText.indexOf("Middle entry")); expect(viewerText.indexOf("Middle entry")).toBeLessThan(viewerText.indexOf("Newest entry")); }); it("renders tool details collapsed by default", async () => { const latestRun = { id: "run-1003", agentId: "agent-001", startedAt: "2024-01-01T00:00:00.000Z", endedAt: null, status: "active", } as AgentHeartbeatRun; mockFetchAgent.mockResolvedValue(createMockAgent({ taskId: undefined, activeRun: latestRun, completedRuns: [], })); mockFetchAgentRuns.mockResolvedValue([latestRun]); mockFetchAgentRunLogs.mockResolvedValue([ { timestamp: "2024-01-01T00:00:00.000Z", taskId: "agent-run", type: "tool", text: "ls -la packages/", detail: "very long tool output", }, ]); render( ); fireEvent.click(await screen.findByText("Logs")); await screen.findByText("ls -la packages/"); const toggle = await screen.findByTestId("tool-detail-toggle"); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument(); }); }); describe("Tasks tab", () => { it("renders tasks returned by fetchAgentTasks", async () => { const user = userEvent.setup(); mockFetchAgentTasks.mockResolvedValue([ { id: "FN-201", title: "Implement assignment API", description: "", column: "in-progress", status: "executing", steps: [], dependencies: [], log: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z", }, ] as any); render( ); await user.click(await screen.findByText("Tasks")); await waitFor(() => { expect(mockFetchAgentTasks).toHaveBeenCalledWith("agent-001", undefined); expect(screen.getByText("FN-201")).toBeInTheDocument(); expect(screen.getByText("Implement assignment API")).toBeInTheDocument(); }); }); it("shows empty state when no tasks are assigned", async () => { const user = userEvent.setup(); mockFetchAgentTasks.mockResolvedValue([]); render( ); await user.click(await screen.findByText("Tasks")); await waitFor(() => { expect(screen.getByText("No tasks assigned to this agent")).toBeInTheDocument(); }); }); }); // ── Advanced Settings (Config Tab) ──────────────────────────────────── describe("Runs Tab — click to show logs", () => { it("shows cache hit ratio section for permanent agents", async () => { const user = userEvent.setup(); mockFetchAgentPromptSizes.mockResolvedValueOnce([ { runId: "run-1", createdAt: "2026-05-14T00:00:00.000Z", systemChars: 1500, execChars: 4800, totalChars: 6300 }, { runId: "run-2", createdAt: "2026-05-13T23:00:00.000Z", systemChars: 1200, execChars: 3900, totalChars: 5100 }, ]); render(); await user.click(await screen.findByText("Runs")); await waitFor(() => { expect(screen.getByText("Cache hit ratio")).toBeInTheDocument(); expect(screen.getByText(/Last 24h:/)).toBeInTheDocument(); expect(screen.getAllByText(/33.3%/)).toHaveLength(3); expect(screen.getByText("Prompt Size")).toBeInTheDocument(); expect(screen.getByText("1,500 / 4,800 / 6,300")).toBeInTheDocument(); }); }); it("hides cache hit ratio section for ephemeral agents", async () => { mockFetchAgentPromptSizes.mockRejectedValueOnce(new Error("400 Prompt sizes are not available for ephemeral agents")); const user = userEvent.setup(); mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: { type: "spawned" } as any })); vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url.includes("/token-usage")) { return new Response(JSON.stringify({ error: "Token usage is not available for ephemeral agents" }), { status: 400 }); } return new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }); })); render(); await user.click(await screen.findByText("Runs")); await waitFor(() => { expect(screen.queryByText("Cache hit ratio")).not.toBeInTheDocument(); }); }); const navigateToRuns = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Runs")).toBeInTheDocument(); }); await user.click(screen.getByText("Runs")); }; it("shows run cards as clickable with chevron indicators", async () => { const user = userEvent.setup(); render( ); await navigateToRuns(user); await waitFor(() => { // Completed run card should be clickable (has role="button") const buttons = screen.getAllByRole("button"); const runButtons = buttons.filter(btn => btn.getAttribute("aria-label")?.includes("run")); expect(runButtons.length).toBeGreaterThan(0); }); }); it("keeps the active run log stream subscribed across run-list polling", async () => { const intervalCallbacks: Array<() => void> = []; const setIntervalSpy = vi.spyOn(globalThis, "setInterval").mockImplementation(((callback: TimerHandler) => { if (typeof callback === "function") { intervalCallbacks.push(callback as () => void); } return 1 as ReturnType; }) as typeof setInterval); const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval").mockImplementation(((id?: ReturnType) => { void id; }) as typeof clearInterval); try { const activeRun = { id: "run-live-1", agentId: "agent-001", startedAt: "2024-01-01T00:00:00.000Z", endedAt: null, status: "active", } as AgentHeartbeatRun; mockFetchAgent.mockResolvedValue(createMockAgent({ activeRun, completedRuns: [], })); mockFetchAgentRuns.mockResolvedValue([activeRun]); mockFetchAgentRunLogs.mockResolvedValue([]); mockFetchAgentRunDetail.mockResolvedValue(activeRun); render( ); await waitFor(() => { expect(screen.getByText("Runs")).toBeInTheDocument(); }); fireEvent.click(screen.getByText("Runs")); await waitFor(() => { expect(screen.getByText("Live Run")).toBeInTheDocument(); }); const activeRunButton = screen.getAllByRole("button").find( (btn) => btn.getAttribute("aria-label")?.includes("run-live") && btn.getAttribute("aria-label")?.includes("active"), ); expect(activeRunButton).toBeTruthy(); fireEvent.click(activeRunButton!); await waitFor(() => { expect(mockFetchAgentRunLogs).toHaveBeenCalledWith("agent-001", "run-live-1", undefined); }); const streamUrl = "/api/agents/agent-001/runs/run-live-1/logs/stream"; expect( mockSubscribeSse.mock.calls.filter(([url]) => url === streamUrl), ).toHaveLength(1); await act(async () => { intervalCallbacks.forEach((callback) => callback()); await Promise.resolve(); }); await waitFor(() => { expect(mockFetchAgentRuns.mock.calls.length).toBeGreaterThanOrEqual(2); }); expect( mockSubscribeSse.mock.calls.filter(([url]) => url === streamUrl), ).toHaveLength(1); } finally { setIntervalSpy.mockRestore(); clearIntervalSpy.mockRestore(); } }); it("fetches and displays logs when clicking a completed run", async () => { const mockLogs: AgentLogEntry[] = [ { timestamp: "2024-01-01T00:01:00.000Z", taskId: "FN-001", text: "Starting task execution", type: "text" }, { timestamp: "2024-01-01T00:02:00.000Z", taskId: "FN-001", text: "Read file: src/index.ts", type: "tool" }, ]; mockFetchAgentRunLogs.mockResolvedValue(mockLogs); const user = userEvent.setup(); render( ); await navigateToRuns(user); // Wait for run cards to render await waitFor(() => { const runButtons = screen.getAllByRole("button").filter( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") ); expect(runButtons.length).toBeGreaterThan(0); }); // Click the completed run const completedRunButton = screen.getAllByRole("button").find( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") )!; await user.click(completedRunButton); // Verify fetchAgentRunLogs was called await waitFor(() => { expect(mockFetchAgentRunLogs).toHaveBeenCalled(); }); // Verify logs appear await waitFor(() => { expect(screen.getByText("Starting task execution")).toBeInTheDocument(); }); }); it("shows loading state while fetching run logs", async () => { // Create a promise that won't resolve immediately let resolveLogs: (value: any) => void; mockFetchAgentRunLogs.mockImplementation(() => new Promise(r => { resolveLogs = r; })); const user = userEvent.setup(); render( ); await navigateToRuns(user); await waitFor(() => { const runButtons = screen.getAllByRole("button").filter( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") ); expect(runButtons.length).toBeGreaterThan(0); }); const completedRunButton = screen.getAllByRole("button").find( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") )!; await user.click(completedRunButton); // Should show loading state await waitFor(() => { expect(screen.getByText("Loading logs...")).toBeInTheDocument(); }); // Resolve to clean up resolveLogs!([]); }); it("shows empty message when no logs available for a run", async () => { mockFetchAgentRunLogs.mockResolvedValue([]); const user = userEvent.setup(); render( ); await navigateToRuns(user); await waitFor(() => { const runButtons = screen.getAllByRole("button").filter( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") ); expect(runButtons.length).toBeGreaterThan(0); }); const completedRunButton = screen.getAllByRole("button").find( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") )!; await user.click(completedRunButton); await waitFor(() => { expect(screen.getByText("No logs available for this run")).toBeInTheDocument(); }); }); it("collapses log viewer when clicking the same run again", async () => { mockFetchAgentRunLogs.mockResolvedValue([ { timestamp: "2024-01-01T00:01:00.000Z", taskId: "FN-001", text: "Test log entry", type: "text" }, ]); const user = userEvent.setup(); render( ); await navigateToRuns(user); await waitFor(() => { const runButtons = screen.getAllByRole("button").filter( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") ); expect(runButtons.length).toBeGreaterThan(0); }); const completedRunButton = screen.getAllByRole("button").find( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") )!; // Click to expand await user.click(completedRunButton); await waitFor(() => { expect(screen.getByText("Test log entry")).toBeInTheDocument(); }); // Click to collapse await user.click(completedRunButton); await waitFor(() => { expect(screen.queryByText("Test log entry")).not.toBeInTheDocument(); }); }); it("shows toast on fetch error", async () => { const addToast = vi.fn(); mockFetchAgentRunLogs.mockRejectedValue(new Error("Network error")); mockFetchAgentRunDetail.mockRejectedValue(new Error("Network error")); const user = userEvent.setup(); render( ); await navigateToRuns(user); await waitFor(() => { const runButtons = screen.getAllByRole("button").filter( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") ); expect(runButtons.length).toBeGreaterThan(0); }); const completedRunButton = screen.getAllByRole("button").find( btn => btn.getAttribute("aria-label")?.includes("run") && btn.getAttribute("aria-label")?.includes("completed") )!; await user.click(completedRunButton); await waitFor(() => { expect(addToast).toHaveBeenCalledWith( expect.stringContaining("Failed to load run details"), "error", ); }); }); }); // ── Instructions Tab ────────────────────────────────────────────────────── });