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, 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 — skills and procedure", () => { beforeEach(() => { setupAgentDetailMocks(); }); describe("Skills", () => { it("renders skill badges in Dashboard tab when agent has skills", async () => { const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1", "skill-2"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); render( , ); await waitFor(() => { expect(screen.getByText("skill-1")).toBeInTheDocument(); expect(screen.getByText("skill-2")).toBeInTheDocument(); }); const skillBadges = document.querySelectorAll(".dashboard-summary-skill-badge"); expect(skillBadges).toHaveLength(2); }); it("loads and displays skill details when a dashboard skill badge is clicked", async () => { const user = userEvent.setup(); const agentWithSkills = createMockAgent({ metadata: { skills: ["/Users/test/.agents/skills/fusion/SKILL.md"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); mockFetchSkillContent.mockResolvedValue({ name: "Fusion Skill", skillMd: "# Fusion Skill", files: [], }); render( , ); const badge = await screen.findByRole("button", { name: "View details for fusion" }); await user.click(badge); await waitFor(() => { expect(mockFetchSkillContent).toHaveBeenCalledWith("/Users/test/.agents/skills/fusion/SKILL.md", undefined); expect(screen.getByText("# Fusion Skill")).toBeInTheDocument(); }); }); it("shows error state and supports retry when skill content loading fails", async () => { const user = userEvent.setup(); const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); mockFetchSkillContent .mockRejectedValueOnce(new Error("Failed to load skill content")) .mockResolvedValueOnce({ name: "Recovered", skillMd: "# Recovered", files: [] }); render( , ); await user.click(await screen.findByRole("button", { name: "View details for skill-1" })); await waitFor(() => { expect(screen.getByText("Failed to load skill content")).toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "Retry" })); await waitFor(() => { expect(screen.getByText("# Recovered")).toBeInTheDocument(); }); }); it("shows fallback when skill content has no SKILL.md body", async () => { const user = userEvent.setup(); const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1"] } }); mockFetchAgent.mockResolvedValue(agentWithSkills); mockFetchSkillContent.mockResolvedValue({ name: "Test Skill", skillMd: "", files: [] }); render( , ); await user.click(await screen.findByRole("button", { name: "View details for skill-1" })); await waitFor(() => { expect(screen.getByText("(No SKILL.md found)")).toBeInTheDocument(); }); }); it("shows dash when agent has no skills in Dashboard tab", async () => { const agentWithNoSkills = createMockAgent({ metadata: {}, }); mockFetchAgent.mockResolvedValue(agentWithNoSkills); render( , ); await waitFor(() => { expect(screen.getByText("Skills: —")).toBeInTheDocument(); }); }); it("shows SkillMultiselect in Config tab", async () => { const user = userEvent.setup(); const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); render( , ); // Navigate to Settings tab await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); await waitFor(() => { expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument(); }); // Should show pre-selected skill expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-1"); }); it("pre-fills skills from agent metadata in Config tab", async () => { const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1", "skill-2"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); const user = userEvent.setup(); render( , ); await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); await waitFor(() => { expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument(); }); // Should have both skills pre-selected expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-1"); expect(screen.getByTestId("skill-multiselect-value").textContent).toContain("skill-2"); }); it("includes skills in metadata when saving Config tab", async () => { const agentWithSkills = createMockAgent({ metadata: { skills: ["skill-1"] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); mockUpdateAgent.mockResolvedValue(createMockAgent({ metadata: { skills: ["skill-1", "new-skill"] } }) as any); const user = userEvent.setup(); render( , ); await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); await waitFor(() => { expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument(); }); // Add a skill await user.click(screen.getByTestId("add-skill-test")); // Save settings await user.click(screen.getByText("Save Settings")); await waitFor(() => { expect(mockUpdateAgent).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ metadata: expect.objectContaining({ skills: ["skill-1", "test-skill"], }), }), undefined, ); }); }); it("enables Save Settings when skills change", async () => { const agentWithSkills = createMockAgent({ metadata: { skills: [] }, }); mockFetchAgent.mockResolvedValue(agentWithSkills); const user = userEvent.setup(); render( , ); await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); await waitFor(() => { expect(screen.getByTestId("skill-multiselect")).toBeInTheDocument(); }); // Initially no changes expect(screen.getByText("Save Settings")).toBeDisabled(); // Add a skill await user.click(screen.getByTestId("add-skill-test")); // Save button should now be enabled await waitFor(() => { expect(screen.getByText("Save Settings")).not.toBeDisabled(); }); }); }); describe("Heartbeat procedure file viewer", () => { const openSettings = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Settings")).toBeInTheDocument(); }); await user.click(screen.getByText("Settings")); }; it("renders heartbeat markdown view action when heartbeatProcedurePath is set", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md", })); const user = userEvent.setup(); render(); await openSettings(user); expect(screen.getByRole("button", { name: "View Heartbeat Markdown" })).toBeInTheDocument(); }); it("fetches and displays heartbeat file content from project workspace", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md", })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "# Heartbeat\n\nDo checks", mtime: "2024-01-01T00:00:00.000Z", size: 20 }); const user = userEvent.setup(); render(); await openSettings(user); await user.click(screen.getByRole("button", { name: "View Heartbeat Markdown" })); await waitFor(() => { expect(mockFetchWorkspaceFileContent).toHaveBeenCalledWith("project", ".fusion/agents/agent-001/HEARTBEAT.md", "proj-1"); }); expect(screen.getByLabelText("Heartbeat Procedure File")).toHaveValue("# Heartbeat\n\nDo checks"); }); it("shows load error feedback when heartbeat file fetch fails", async () => { const addToast = vi.fn(); mockFetchAgent.mockResolvedValue(createMockAgent({ heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md", })); mockFetchWorkspaceFileContent.mockRejectedValue(new Error("permission denied")); const user = userEvent.setup(); render(); await openSettings(user); await user.click(screen.getByRole("button", { name: "View Heartbeat Markdown" })); await waitFor(() => { expect(addToast).toHaveBeenCalledWith("Failed to load heartbeat procedure file: permission denied", "error"); }); expect(screen.getByText("Failed to load file: permission denied")).toBeInTheDocument(); }); it("refreshes to upgraded heartbeat path and supports immediate viewing", async () => { mockFetchAgent .mockResolvedValueOnce(createMockAgent({ heartbeatProcedurePath: undefined })) .mockResolvedValueOnce(createMockAgent({ heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md" })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "# Seeded", mtime: "2024-01-01T00:00:00.000Z", size: 8 }); const user = userEvent.setup(); render(); await openSettings(user); await user.click(screen.getByRole("button", { name: "Upgrade agent to default heartbeat procedure file" })); await waitFor(() => { expect(mockUpgradeAgentHeartbeatProcedure).toHaveBeenCalledWith("agent-001", "proj-2"); }); await waitFor(() => { expect(screen.getByRole("button", { name: "View Heartbeat Markdown" })).toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "View Heartbeat Markdown" })); await waitFor(() => { expect(mockFetchWorkspaceFileContent).toHaveBeenCalledWith("project", ".fusion/agents/agent-001/HEARTBEAT.md", "proj-2"); }); }); }); });