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 — editors", () => { beforeEach(() => { setupAgentDetailMocks(); }); describe("Instructions Tab", () => { const navigateToInstructions = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Instructions")).toBeInTheDocument(); }); await user.click(screen.getByText("Instructions")); }; it("renders Instructions tab with inline instructions and path fields", async () => { const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.getByLabelText("Inline Instructions")).toBeInTheDocument(); expect(screen.getByLabelText("Instructions File Path")).toBeInTheDocument(); }); }); it("does not show file editor when instructions path is empty", async () => { const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.queryByLabelText("File Content")).not.toBeInTheDocument(); }); }); it("shows file editor when instructions path is set", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test-agent.md", })); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.getByLabelText("File Content")).toBeInTheDocument(); }); }); it("calls fetchWorkspaceFileContent when instructions path is set", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test-agent.md", })); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(mockFetchWorkspaceFileContent).toHaveBeenCalledWith("project", ".fusion/agents/test-agent.md"); }); }); it("shows file content when fetchWorkspaceFileContent succeeds", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test-agent.md", })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "# Test Agent Instructions\n\nThese are the agent instructions.", mtime: "2024-01-01T00:00:00.000Z", size: 60, }); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.getByLabelText("File Content")).toHaveValue("# Test Agent Instructions\n\nThese are the agent instructions."); }); }); it("shows error toast when fetchWorkspaceFileContent fails with non-ENOENT error", async () => { const addToast = vi.fn(); mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test-agent.md", })); mockFetchWorkspaceFileContent.mockRejectedValue(new Error("Permission denied")); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(addToast).toHaveBeenCalledWith( expect.stringContaining("Failed to load instructions file"), "error", ); }); }); it("treats ENOENT as empty file (new file state)", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/new-agent.md", })); mockFetchWorkspaceFileContent.mockRejectedValue(new Error("ENOENT: file not found")); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { // Should show empty content (new file state), not show error toast const fileContent = screen.getByLabelText("File Content") as HTMLTextAreaElement; expect(fileContent.value).toBe(""); }); }); it("calls updateAgentInstructions with expected payload when saving inline instructions", async () => { const addToast = vi.fn(); mockUpdateAgentInstructions.mockResolvedValue({} as any); const user = userEvent.setup(); render( ); await navigateToInstructions(user); const instructionsTextarea = await screen.findByLabelText("Inline Instructions"); await user.clear(instructionsTextarea); await user.type(instructionsTextarea, "Custom instructions for the agent"); const pathInput = await screen.findByLabelText("Instructions File Path"); await user.clear(pathInput); await user.type(pathInput, ".fusion/agents/test.md"); await user.click(screen.getByText("Save Instructions")); await waitFor(() => { expect(mockUpdateAgentInstructions).toHaveBeenCalledWith( "agent-001", { instructionsText: "Custom instructions for the agent", instructionsPath: ".fusion/agents/test.md", }, undefined, ); }); expect(addToast).toHaveBeenCalledWith("Instructions saved", "success"); }); it("calls saveWorkspaceFileContent when saving file content", async () => { const addToast = vi.fn(); const onMutationSuccess = vi.fn(); mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test.md", })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "Original content", mtime: "2024-01-01T00:00:00.000Z", size: 16, }); const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Wait for file content to load await waitFor(() => { expect(screen.getByLabelText("File Content")).toHaveValue("Original content"); }); // Modify file content const fileContent = screen.getByLabelText("File Content"); await user.clear(fileContent); await user.type(fileContent, "Updated content"); // Save file await user.click(screen.getByText("Save File")); await waitFor(() => { expect(mockSaveWorkspaceFileContent).toHaveBeenCalledWith( "project", ".fusion/agents/test.md", "Updated content", ); expect(onMutationSuccess).toHaveBeenCalledWith({ agentId: "agent-001", deleted: false }); }); expect(addToast).toHaveBeenCalledWith("Instructions file saved", "success"); }); it("disables Save Instructions button when no changes", async () => { const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.getByText("Save Instructions")).toBeDisabled(); }); }); it("disables Save File button when file content is not dirty", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test.md", })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "Original content", mtime: "2024-01-01T00:00:00.000Z", size: 16, }); const user = userEvent.setup(); render( ); await navigateToInstructions(user); await waitFor(() => { expect(screen.getByText("Save File")).toBeDisabled(); }); }); it("shows Unsaved changes indicator when file content is dirty", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test.md", })); mockFetchWorkspaceFileContent.mockResolvedValue({ content: "Original content", mtime: "2024-01-01T00:00:00.000Z", size: 16, }); const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Wait for file content to load await waitFor(() => { expect(screen.getByLabelText("File Content")).toHaveValue("Original content"); }); // Modify file content const fileContent = screen.getByLabelText("File Content"); await user.clear(fileContent); await user.type(fileContent, "Modified content"); await waitFor(() => { expect(screen.getByText("Unsaved changes")).toBeInTheDocument(); }); }); it("forwards projectId to updateAgentInstructions", async () => { const addToast = vi.fn(); const user = userEvent.setup(); render( ); await navigateToInstructions(user); const instructionsTextarea = await screen.findByLabelText("Inline Instructions"); await user.clear(instructionsTextarea); await user.type(instructionsTextarea, "Custom instructions"); await user.click(screen.getByText("Save Instructions")); await waitFor(() => { expect(mockUpdateAgentInstructions).toHaveBeenCalledWith( "agent-001", expect.objectContaining({ instructionsText: "Custom instructions", }), "proj_456", ); }); }); it("toggles between edit and preview mode for inline instructions", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsText: "# Test\n\nThis is a test.", })); const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Default: edit mode should be active - verify textarea is present await waitFor(() => { expect(screen.getByLabelText("Inline Instructions")).toBeInTheDocument(); }); // Find and verify the toggle buttons exist const previewBtn = screen.getByTestId("instructions-preview-toggle"); expect(previewBtn).toBeInTheDocument(); // Click Preview button await user.click(previewBtn); // After clicking, the textarea should be gone and preview should appear await waitFor(() => { expect(screen.queryByLabelText("Inline Instructions")).not.toBeInTheDocument(); }); // Check for markdown preview const preview = document.querySelector(".markdown-body"); expect(preview).toBeInTheDocument(); // Click Edit button to go back const editBtn = screen.getByTestId("instructions-edit-toggle"); await user.click(editBtn); // Should be back in edit mode await waitFor(() => { expect(screen.getByLabelText("Inline Instructions")).toBeInTheDocument(); }); }); it("renders markdown content in preview mode for inline instructions", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsText: "# Test Instructions\n\nThis is **bold** and this is _italic_.", })); const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Click Preview button await user.click(screen.getByTestId("instructions-preview-toggle")); await waitFor(() => { // Should render markdown elements expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Test Instructions"); expect(document.querySelector(".markdown-body")).toBeInTheDocument(); }); }); it("shows placeholder when inline instructions are empty in preview mode", async () => { const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Click Preview button when instructions are empty await user.click(screen.getByTestId("instructions-preview-toggle")); await waitFor(() => { expect(screen.getByText("No inline instructions defined yet. Switch to Edit mode to add instructions.")).toBeInTheDocument(); }); }); it("hides save button when in preview mode for inline instructions", async () => { const user = userEvent.setup(); render( ); await navigateToInstructions(user); // Save button should be visible in edit mode await waitFor(() => { expect(screen.getByText("Save Instructions")).toBeInTheDocument(); }); // Click Preview button await user.click(screen.getByTestId("instructions-preview-toggle")); // Save button should be hidden await waitFor(() => { expect(screen.queryByText("Save Instructions")).not.toBeInTheDocument(); }); }); it("does not affect file path section when toggling inline instructions preview", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ instructionsPath: ".fusion/agents/test-agent.md", })); const user = userEvent.setup(); render( ); await navigateToInstructions(user); // File path section should be visible await waitFor(() => { expect(screen.getByLabelText("Instructions File Path")).toBeInTheDocument(); }); // Toggle to preview mode await user.click(screen.getByTestId("instructions-preview-toggle")); // File path should still be visible await waitFor(() => { expect(screen.getByLabelText("Instructions File Path")).toBeInTheDocument(); }); // Toggle back to edit mode await user.click(screen.getByTestId("instructions-edit-toggle")); // File path should still be visible await waitFor(() => { expect(screen.getByLabelText("Instructions File Path")).toBeInTheDocument(); }); }); }); // ── Soul Tab ──────────────────────────────────────────────────────────────── describe("Soul Tab", () => { const navigateToSoul = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Soul")).toBeInTheDocument(); }); await user.click(screen.getByText("Soul")); }; it("renders Soul tab with textarea by default", async () => { const user = userEvent.setup(); render( ); await navigateToSoul(user); await waitFor(() => { expect(screen.getByLabelText("Agent Soul")).toBeInTheDocument(); expect(screen.getByText("Edit")).toBeInTheDocument(); expect(screen.getByText("Preview")).toBeInTheDocument(); }); }); it("toggles between edit and preview mode", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ soul: "# Agent Soul\n\nThis agent is **helpful** and _creative_.", })); const user = userEvent.setup(); render( ); await navigateToSoul(user); // Default: edit mode await waitFor(() => { expect(screen.getByLabelText("Agent Soul")).toBeInTheDocument(); }); // Click Preview await user.click(screen.getByText("Preview")); await waitFor(() => { expect(screen.queryByLabelText("Agent Soul")).not.toBeInTheDocument(); expect(document.querySelector(".markdown-body")).toBeInTheDocument(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Agent Soul"); }); // Click Edit await user.click(screen.getByText("Edit")); await waitFor(() => { expect(screen.getByLabelText("Agent Soul")).toBeInTheDocument(); }); }); it("shows placeholder when soul is empty in preview mode", async () => { const user = userEvent.setup(); render( ); await navigateToSoul(user); await user.click(screen.getByText("Preview")); await waitFor(() => { expect(screen.getByText("No soul defined yet. Switch to Edit mode to define the agent's personality.")).toBeInTheDocument(); }); }); it("hides save button when in preview mode", async () => { const user = userEvent.setup(); render( ); await navigateToSoul(user); await waitFor(() => { expect(screen.getByText("Save Soul")).toBeInTheDocument(); }); await user.click(screen.getByText("Preview")); await waitFor(() => { expect(screen.queryByText("Save Soul")).not.toBeInTheDocument(); }); }); it("calls updateAgentSoul when saving soul", async () => { const addToast = vi.fn(); mockUpdateAgentSoul.mockResolvedValue({} as any); const user = userEvent.setup(); render( ); await navigateToSoul(user); const textarea = await screen.findByLabelText("Agent Soul"); await user.clear(textarea); await user.type(textarea, "This is the agent's new soul"); await user.click(screen.getByText("Save Soul")); await waitFor(() => { expect(mockUpdateAgentSoul).toHaveBeenCalledWith("agent-001", "This is the agent's new soul", undefined); expect(addToast).toHaveBeenCalledWith("Soul saved", "success"); }); }); }); // ── Memory Tab ───────────────────────────────────────────────────────────── describe("Memory Tab", () => { const navigateToMemory = async (user: ReturnType) => { await waitFor(() => { expect(screen.getByText("Agent Memory")).toBeInTheDocument(); }); await user.click(screen.getByText("Agent Memory")); }; it("renders Memory tab with textarea by default", async () => { const user = userEvent.setup(); render(); await navigateToMemory(user); await waitFor(() => { expect(screen.getByLabelText("Agent Memory")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Edit mode" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Preview mode" })).toBeInTheDocument(); }); }); it("toggles between edit and preview mode", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ memory: "# Agent Memory\n\n- Item 1\n- Item 2" })); const user = userEvent.setup(); render(); await navigateToMemory(user); await user.click(screen.getByRole("button", { name: "Preview mode" })); await waitFor(() => { expect(screen.queryByLabelText("Agent Memory")).not.toBeInTheDocument(); expect(document.querySelector(".markdown-body")).toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "Edit mode" })); await waitFor(() => { expect(screen.getByLabelText("Agent Memory")).toBeInTheDocument(); }); }); it("shows placeholder when memory is empty in preview mode", async () => { const user = userEvent.setup(); render(); await navigateToMemory(user); await user.click(screen.getByRole("button", { name: "Preview mode" })); await waitFor(() => { expect(screen.getByText("No agent memory defined yet. Switch to Edit mode to add memory content.")).toBeInTheDocument(); }); }); it("hides save button when in preview mode", async () => { const user = userEvent.setup(); render(); await navigateToMemory(user); expect(screen.getByText("Save Memory")).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Preview mode" })); await waitFor(() => expect(screen.queryByText("Save Memory")).not.toBeInTheDocument()); }); it("hides inline Edit button when agent is running", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ state: "running", memory: "This agent has memory." })); const user = userEvent.setup(); render(); await navigateToMemory(user); await waitFor(() => { expect(screen.getByRole("button", { name: "Preview mode" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Edit mode" })).not.toBeInTheDocument(); }); }); it("can switch inline memory to preview mode when agent is running", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ state: "running", memory: "Agent memory content" })); const user = userEvent.setup(); render(); await navigateToMemory(user); await user.click(screen.getByRole("button", { name: "Preview mode" })); await waitFor(() => expect(document.querySelector(".markdown-body")).toBeInTheDocument()); }); it("renders memory file preview markdown and toggles back to edit", async () => { mockFetchAgentMemoryFile.mockResolvedValue({ path: ".fusion/agent-memory/agent-001/MEMORY.md", content: "# Heading\n\n- entry" } as any); const user = userEvent.setup(); render(); await navigateToMemory(user); await user.click(await screen.findByRole("button", { name: "Memory file preview mode" })); await waitFor(() => { expect(screen.queryByPlaceholderText("Select a memory file to view and edit its content...")).not.toBeInTheDocument(); expect(screen.getByText("Heading")).toBeInTheDocument(); expect(screen.queryByText("Save Memory File")).not.toBeInTheDocument(); }); await user.click(screen.getByRole("button", { name: "Memory file edit mode" })); await waitFor(() => { expect(screen.getByPlaceholderText("Select a memory file to view and edit its content...")).toBeInTheDocument(); }); }); it("shows memory file preview placeholder when selected file is empty", async () => { mockFetchAgentMemoryFile.mockResolvedValue({ path: ".fusion/agent-memory/agent-001/MEMORY.md", content: "" } as any); const user = userEvent.setup(); render(); await navigateToMemory(user); await user.click(await screen.findByRole("button", { name: "Memory file preview mode" })); await waitFor(() => { expect(screen.getByText("No memory file content yet. Switch to Edit mode to add content.")).toBeInTheDocument(); }); }); it("hides memory file edit button and disables save button for running agents", async () => { mockFetchAgent.mockResolvedValue(createMockAgent({ state: "running" })); const user = userEvent.setup(); render(); await navigateToMemory(user); await waitFor(() => { expect(screen.getByRole("button", { name: "Memory file preview mode" })).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Memory file edit mode" })).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "Save Memory File" })).toBeDisabled(); }); }); it("calls updateAgentMemory when saving memory", async () => { const addToast = vi.fn(); mockUpdateAgentMemory.mockResolvedValue({} as any); const user = userEvent.setup(); render(); await navigateToMemory(user); const textarea = await screen.findByLabelText("Agent Memory"); await user.clear(textarea); await user.type(textarea, "This is the agent's new memory"); await user.click(screen.getByText("Save Memory")); await waitFor(() => { expect(mockUpdateAgentMemory).toHaveBeenCalledWith("agent-001", "This is the agent's new memory", undefined); expect(addToast).toHaveBeenCalledWith("Memory saved", "success"); }); }); }); // ── Skills ───────────────────────────────────────────────────────────────── });