feat(FN-1558): add dedicated Instructions tab in Agent Detail view

- Add InstructionsTab component for editing agent system instructions directly in the dashboard
- Include validation (character count, preview section) and reset to default functionality
- Add 82 regression tests covering Instructions tab editing flows
- Update agents.md documentation with Instructions tab usage guide
- Add changeset for @gsxdsm/fusion package
This commit is contained in:
gsxdsm
2026-04-10 13:30:37 -07:00
parent df1ff16519
commit 775defd1eb
4 changed files with 679 additions and 108 deletions

View File

@@ -3,10 +3,10 @@ import {
Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw,
Settings, FileText, ActivitySquare, X, Copy,
ExternalLink, CheckCircle, XCircle, Loader2, GitBranch, ListChecks,
ChevronDown, ChevronRight, BarChart3, Star
ChevronDown, ChevronRight, BarChart3, Star, BookOpen
} from "lucide-react";
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget } from "../api";
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogs, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent } from "../api";
import type { Agent } from "../api";
import type { AgentLogEntry, Task } from "@fusion/core";
import { AgentLogViewer } from "./AgentLogViewer";
@@ -52,7 +52,7 @@ interface AgentDetailViewProps {
onChildClick?: (childId: string) => void;
}
type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "memory" | "reflections" | "performance";
type TabId = "dashboard" | "logs" | "config" | "runs" | "tasks" | "employees" | "soul" | "instructions" | "memory" | "reflections" | "performance";
const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
{ id: "dashboard", label: "Dashboard", icon: ActivitySquare },
@@ -61,6 +61,7 @@ const TABS: { id: TabId; label: string; icon: typeof Activity }[] = [
{ id: "tasks", label: "Tasks", icon: ListChecks },
{ id: "employees", label: "Employees", icon: GitBranch },
{ id: "soul", label: "Soul", icon: Heart },
{ id: "instructions", label: "Instructions", icon: BookOpen },
{ id: "memory", label: "Memory", icon: FileText },
{ id: "reflections", label: "Reflections", icon: BarChart3 },
{ id: "performance", label: "Performance", icon: Star },
@@ -427,6 +428,15 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
/>
)}
{activeTab === "instructions" && (
<InstructionsTab
agent={agent}
projectId={projectId}
addToast={addToast}
onSaved={loadAgent}
/>
)}
{activeTab === "memory" && (
<MemoryTab
agent={agent}
@@ -1654,6 +1664,265 @@ function MemoryTab({
);
}
function InstructionsTab({
agent,
projectId,
addToast,
onSaved,
}: {
agent: AgentDetail;
projectId?: string;
addToast: (message: string, type?: "success" | "error") => void;
onSaved: () => Promise<void>;
}) {
// Inline instructions state
const [instructionsText, setInstructionsText] = useState(agent.instructionsText ?? "");
const [instructionsPath, setInstructionsPath] = useState(agent.instructionsPath ?? "");
// File content state (when instructionsPath is set)
const [fileContent, setFileContent] = useState("");
const [isLoadingFile, setIsLoadingFile] = useState(false);
const [fileContentDirty, setFileContentDirty] = useState(false);
// Save state
const [isSaving, setIsSaving] = useState(false);
const [isSavingFile, setIsSavingFile] = useState(false);
const [justSaved, setJustSaved] = useState(false);
const [justSavedFile, setJustSavedFile] = useState(false);
// Load file content when instructionsPath changes
useEffect(() => {
const path = instructionsPath.trim();
if (!path) {
setFileContent("");
setFileContentDirty(false);
return;
}
setIsLoadingFile(true);
fetchWorkspaceFileContent("project", path)
.then((data) => {
setFileContent(data.content);
setFileContentDirty(false);
})
.catch((err: any) => {
// ENOENT means file doesn't exist yet - treat as empty "new file" state
if (err.message?.includes("ENOENT") || err.message?.includes("Not found") || err.message?.includes("not found")) {
setFileContent("");
setFileContentDirty(false);
} else {
addToast(`Failed to load instructions file: ${err.message}`, "error");
setFileContent("");
}
})
.finally(() => {
setIsLoadingFile(false);
});
}, [instructionsPath, addToast]);
// Sync with agent data changes
useEffect(() => {
setInstructionsText(agent.instructionsText ?? "");
setInstructionsPath(agent.instructionsPath ?? "");
setJustSaved(false);
setJustSavedFile(false);
}, [agent.id, agent.instructionsText, agent.instructionsPath]);
const hasInstructionsChanges = (() => {
const currentText = instructionsText ?? "";
const persistedText = agent.instructionsText ?? "";
const currentPath = instructionsPath?.trim() ?? "";
const persistedPath = agent.instructionsPath?.trim() ?? "";
return currentText !== persistedText || currentPath !== persistedPath;
})();
const handleSaveInstructions = async () => {
setIsSaving(true);
try {
await updateAgentInstructions(
agent.id,
{
instructionsText: instructionsText || undefined,
instructionsPath: instructionsPath.trim() || undefined,
},
projectId,
);
addToast("Instructions saved", "success");
setJustSaved(true);
setTimeout(() => setJustSaved(false), 3000);
await onSaved();
} catch (err: any) {
addToast(`Failed to save instructions: ${err.message}`, "error");
} finally {
setIsSaving(false);
}
};
const handleSaveFile = async () => {
const path = instructionsPath.trim();
if (!path) {
addToast("No instructions file path set", "error");
return;
}
setIsSavingFile(true);
try {
await saveWorkspaceFileContent("project", path, fileContent);
addToast("Instructions file saved", "success");
setFileContentDirty(false);
setJustSavedFile(true);
setTimeout(() => setJustSavedFile(false), 3000);
} catch (err: any) {
addToast(`Failed to save instructions file: ${err.message}`, "error");
} finally {
setIsSavingFile(false);
}
};
const hasFilePath = !!instructionsPath.trim();
return (
<div className="config-tab">
<div className="config-section">
<h3>Custom Instructions</h3>
<p className="config-description">
Append custom instructions to this agent&apos;s system prompt at execution time. Use this to customize behavior, coding style, or project conventions without modifying built-in prompts.
</p>
<div className="config-fields">
<div className="config-field">
<label htmlFor="instructions-text">Inline Instructions</label>
<textarea
id="instructions-text"
className="input"
rows={10}
placeholder="Enter custom instructions to append to this agent's system prompt..."
value={instructionsText}
onChange={(e) => {
setInstructionsText(e.target.value);
setJustSaved(false);
}}
style={{ fontFamily: "monospace", fontSize: "0.875rem", resize: "vertical" }}
/>
<span className="config-hint">Markdown formatting supported. Max 50,000 characters.</span>
</div>
<div className="config-field">
<label htmlFor="instructions-path">Instructions File Path</label>
<input
id="instructions-path"
type="text"
className="input"
placeholder="e.g., .fusion/agents/my-agent-instructions.md"
value={instructionsPath}
onChange={(e) => {
setInstructionsPath(e.target.value);
setJustSaved(false);
}}
/>
<span className="config-hint">Path to a .md file (relative to project root). Contents are read and appended at execution time.</span>
</div>
</div>
<div className="config-actions">
<button
className="btn btn--primary"
disabled={!hasInstructionsChanges || isSaving}
onClick={() => void handleSaveInstructions()}
>
{isSaving ? (
<>
<Loader2 size={16} className="animate-spin" />
Saving
</>
) : (
<>
<CheckCircle size={16} />
Save Instructions
</>
)}
</button>
{!hasInstructionsChanges && justSaved && (
<span className="config-saved-indicator">
<CheckCircle size={14} />
Instructions saved
</span>
)}
</div>
</div>
{hasFilePath && (
<div className="config-section">
<h3>Instructions File Editor</h3>
<p className="config-description">
Edit the instructions file directly. Changes are saved separately from the path configuration.
</p>
<div className="config-fields">
<div className="config-field">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "8px" }}>
<label htmlFor="instructions-file-content">File Content</label>
{isLoadingFile && (
<span className="config-hint" style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<Loader2 size={12} className="animate-spin" />
Loading...
</span>
)}
{fileContentDirty && !isLoadingFile && (
<span className="config-hint" style={{ color: "var(--color-warning, #e3b541)" }}>
Unsaved changes
</span>
)}
</div>
<textarea
id="instructions-file-content"
className="input"
rows={20}
placeholder="File content will appear here when loaded..."
value={fileContent}
readOnly={isLoadingFile}
onChange={(e) => {
setFileContent(e.target.value);
setFileContentDirty(true);
setJustSavedFile(false);
}}
style={{ fontFamily: "monospace", fontSize: "0.875rem", resize: "vertical" }}
/>
<span className="config-hint">Edit the markdown file content directly. Save separately using the button below.</span>
</div>
</div>
<div className="config-actions">
<button
className="btn btn--primary"
disabled={!fileContentDirty || isSavingFile}
onClick={() => void handleSaveFile()}
>
{isSavingFile ? (
<>
<Loader2 size={16} className="animate-spin" />
Saving
</>
) : (
<>
<CheckCircle size={16} />
Save File
</>
)}
</button>
{!fileContentDirty && justSavedFile && (
<span className="config-saved-indicator">
<CheckCircle size={14} />
File saved
</span>
)}
</div>
</div>
)}
</div>
);
}
function PerformanceTab({
agentId,
projectId,
@@ -1971,14 +2240,8 @@ function ConfigTab({
};
const [isSaving, setIsSaving] = useState(false);
const [isSavingInstructions, setIsSavingInstructions] = useState(false);
const [errors, setErrors] = useState<ValidationErrors>({});
const [justSaved, setJustSaved] = useState(false);
const [justSavedInstructions, setJustSavedInstructions] = useState(false);
// Custom instructions state
const [instructionsText, setInstructionsText] = useState(agent.instructionsText ?? "");
const [instructionsPath, setInstructionsPath] = useState(agent.instructionsPath ?? "");
/** Detect whether any local value differs from the persisted metadata */
const hasChanges = (() => {
@@ -2015,14 +2278,6 @@ function ConfigTab({
return false;
})();
const hasInstructionsChanges = (() => {
const currentText = instructionsText ?? "";
const persistedText = agent.instructionsText ?? "";
const currentPath = instructionsPath?.trim() ?? "";
const persistedPath = agent.instructionsPath?.trim() ?? "";
return currentText !== persistedText || currentPath !== persistedPath;
})();
const handleFieldChange = (key: string, value: string) => {
setFormValues((prev) => ({ ...prev, [key]: value }));
setJustSaved(false);
@@ -2209,28 +2464,6 @@ function ConfigTab({
}
};
const handleSaveInstructions = async () => {
setIsSavingInstructions(true);
try {
await updateAgentInstructions(
agent.id,
{
instructionsText: instructionsText || undefined,
instructionsPath: instructionsPath.trim() || undefined,
},
projectId,
);
addToast("Instructions saved", "success");
setJustSavedInstructions(true);
setTimeout(() => setJustSavedInstructions(false), 3000);
await onSaved();
} catch (err: any) {
addToast(`Failed to save instructions: ${err.message}`, "error");
} finally {
setIsSavingInstructions(false);
}
};
return (
<div className="config-tab">
<div className="config-section">
@@ -2559,74 +2792,6 @@ function ConfigTab({
)}
</div>
</div>
<div className="config-section">
<h3>Custom Instructions</h3>
<p className="config-description">
Append custom instructions to this agent&apos;s system prompt at execution time. Use this to customize behavior, coding style, or project conventions without modifying built-in prompts.
</p>
<div className="config-fields">
<div className="config-field">
<label htmlFor="instructions-text">Inline Instructions</label>
<textarea
id="instructions-text"
className="input"
rows={10}
placeholder="Enter custom instructions to append to this agent's system prompt..."
value={instructionsText}
onChange={(e) => {
setInstructionsText(e.target.value);
setJustSavedInstructions(false);
}}
style={{ fontFamily: "monospace", fontSize: "0.875rem", resize: "vertical" }}
/>
<span className="config-hint">Markdown formatting supported. Max 50,000 characters.</span>
</div>
<div className="config-field">
<label htmlFor="instructions-path">Instructions File Path</label>
<input
id="instructions-path"
type="text"
className="input"
placeholder="e.g., .fusion/agents/my-agent-instructions.md"
value={instructionsPath}
onChange={(e) => {
setInstructionsPath(e.target.value);
setJustSavedInstructions(false);
}}
/>
<span className="config-hint">Path to a .md file (relative to project root). Contents are read and appended at execution time.</span>
</div>
</div>
<div className="config-actions">
<button
className="btn btn--primary"
disabled={!hasInstructionsChanges || isSavingInstructions}
onClick={() => void handleSaveInstructions()}
>
{isSavingInstructions ? (
<>
<Loader2 size={16} className="animate-spin" />
Saving…
</>
) : (
<>
<CheckCircle size={16} />
Save Instructions
</>
)}
</button>
{!hasInstructionsChanges && justSavedInstructions && (
<span className="config-saved-indicator">
<CheckCircle size={14} />
Instructions saved
</span>
)}
</div>
</div>
</div>
);
}

View File

@@ -24,6 +24,8 @@ vi.mock("../../api", () => ({
fetchChainOfCommand: vi.fn(),
fetchAgentBudgetStatus: vi.fn(),
resetAgentBudget: vi.fn(),
fetchWorkspaceFileContent: vi.fn(),
saveWorkspaceFileContent: vi.fn(),
}));
vi.mock("../AgentLogViewer", () => ({
@@ -34,7 +36,7 @@ vi.mock("../AgentLogViewer", () => ({
),
}));
import { fetchAgent, updateAgent, updateAgentState, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget } from "../../api";
import { fetchAgent, updateAgent, updateAgentState, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, fetchWorkspaceFileContent, saveWorkspaceFileContent } from "../../api";
const mockFetchAgent = vi.mocked(fetchAgent);
const mockUpdateAgent = vi.mocked(updateAgent);
@@ -47,6 +49,9 @@ const mockFetchAgentTasks = vi.mocked(fetchAgentTasks);
const mockFetchChainOfCommand = vi.mocked(fetchChainOfCommand);
const mockFetchAgentBudgetStatus = vi.mocked(fetchAgentBudgetStatus);
const mockResetAgentBudget = vi.mocked(resetAgentBudget);
const mockUpdateAgentInstructions = vi.mocked(updateAgentInstructions);
const mockFetchWorkspaceFileContent = vi.mocked(fetchWorkspaceFileContent);
const mockSaveWorkspaceFileContent = vi.mocked(saveWorkspaceFileContent);
describe("AgentDetailView", () => {
const createMockAgent = (overrides: Partial<{
@@ -115,6 +120,10 @@ describe("AgentDetailView", () => {
nextResetAt: null,
});
mockResetAgentBudget.mockResolvedValue(undefined);
// Default: empty file content
mockFetchWorkspaceFileContent.mockResolvedValue({ content: "", mtime: "2024-01-01T00:00:00.000Z", size: 0 });
mockSaveWorkspaceFileContent.mockResolvedValue({ success: true, mtime: "2024-01-01T00:00:00.000Z", size: 0 });
mockUpdateAgentInstructions.mockResolvedValue({} as any);
});
it("shows loading state initially", () => {
@@ -407,6 +416,7 @@ describe("AgentDetailView", () => {
expect(screen.getByText("Tasks")).toBeInTheDocument();
expect(screen.getByText("Employees")).toBeInTheDocument();
expect(screen.getByText("Soul")).toBeInTheDocument();
expect(screen.getByText("Instructions")).toBeInTheDocument();
expect(screen.getByText("Memory")).toBeInTheDocument();
expect(screen.getByText("Settings")).toBeInTheDocument();
});
@@ -1980,4 +1990,362 @@ describe("AgentDetailView", () => {
});
});
});
// ── Instructions Tab ──────────────────────────────────────────────────────
describe("Instructions Tab", () => {
const navigateToInstructions = async (user: ReturnType<typeof userEvent.setup>) => {
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={addToast}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={addToast}
/>
);
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();
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={addToast}
/>
);
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(addToast).toHaveBeenCalledWith("Instructions file saved", "success");
});
it("disables Save Instructions button when no changes", async () => {
const user = userEvent.setup();
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
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(
<AgentDetailView
agentId="agent-001"
projectId="proj_456"
onClose={vi.fn()}
addToast={addToast}
/>
);
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",
);
});
});
});
});