feat(FN-3030): add heartbeat markdown viewer modal to AgentDetailView
Adds a heartbeat markdown viewer modal to the AgentDetailView (FN-3030) with styling, tests, and documentation for the feature, while also rendering multi-sample theme swatches in the ThemeSelector component (FN-3028). Fusion-Task-Id: FN-3030
This commit is contained in:
@@ -196,6 +196,24 @@ There are two ways to provide custom instructions:
|
|||||||
- The editor has an **Unsaved changes** indicator when file content is modified
|
- The editor has an **Unsaved changes** indicator when file content is modified
|
||||||
- File saves are independent from instruction metadata saves
|
- File saves are independent from instruction metadata saves
|
||||||
|
|
||||||
|
## Heartbeat Procedure File Access (Agent Detail Modal)
|
||||||
|
|
||||||
|
The **Settings** tab in the Agent Detail modal includes a **Heartbeat Procedure** section with an in-modal markdown file viewer/editor.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. The section shows the current `heartbeatProcedurePath`.
|
||||||
|
2. When a path exists, use **View Heartbeat Markdown** to load and inspect that file without leaving the modal.
|
||||||
|
3. The editor supports both **Edit** and **Preview** modes, with an unsaved-changes indicator and dedicated save action.
|
||||||
|
4. Reads/writes are scoped through the workspace file APIs with `projectId` awareness in multi-project mode.
|
||||||
|
|
||||||
|
### Relation to upgrade flow
|
||||||
|
|
||||||
|
- **Upgrade to Default Heartbeat Procedure** still sets `heartbeatProcedurePath` to:
|
||||||
|
- `.fusion/agents/{agent.id}/HEARTBEAT.md`
|
||||||
|
- If the default file does not exist yet, the backend seeds it from the built-in template.
|
||||||
|
- After upgrade completes and the agent refreshes, operators can immediately open the seeded per-agent `HEARTBEAT.md` from the same modal section.
|
||||||
|
|
||||||
## New Agent Presets (Dashboard UI)
|
## New Agent Presets (Dashboard UI)
|
||||||
|
|
||||||
The New Agent dialog keeps the existing 3-step flow, and step 0 is split into two tabs:
|
The New Agent dialog keeps the existing 3-step flow, and step 0 is split into two tabs:
|
||||||
|
|||||||
@@ -743,6 +743,25 @@
|
|||||||
color: var(--color-success);
|
color: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heartbeat-procedure-actions {
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heartbeat-procedure-viewer {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heartbeat-procedure-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heartbeat-procedure-status--warning {
|
||||||
|
color: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
/* === Agent Evaluation Ratings === */
|
/* === Agent Evaluation Ratings === */
|
||||||
|
|
||||||
.rating-summary-card {
|
.rating-summary-card {
|
||||||
@@ -1223,6 +1242,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heartbeat-procedure-status {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
@@ -2521,9 +2521,67 @@ function HeartbeatProcedureSection({
|
|||||||
onSaved: () => Promise<void>;
|
onSaved: () => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
const [isUpgrading, setIsUpgrading] = useState(false);
|
||||||
|
const [showFileViewer, setShowFileViewer] = useState(false);
|
||||||
|
const [isLoadingFile, setIsLoadingFile] = useState(false);
|
||||||
|
const [isSavingFile, setIsSavingFile] = useState(false);
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const [fileContent, setFileContent] = useState("");
|
||||||
|
const [fileContentDirty, setFileContentDirty] = useState(false);
|
||||||
|
const [fileLoadError, setFileLoadError] = useState<string | null>(null);
|
||||||
|
const [justSavedFile, setJustSavedFile] = useState(false);
|
||||||
const currentPath = agent.heartbeatProcedurePath?.trim();
|
const currentPath = agent.heartbeatProcedurePath?.trim();
|
||||||
const expectedDefaultPath = `.fusion/agents/${agent.id}/HEARTBEAT.md`;
|
const expectedDefaultPath = `.fusion/agents/${agent.id}/HEARTBEAT.md`;
|
||||||
const onDefault = currentPath === expectedDefaultPath;
|
const onDefault = currentPath === expectedDefaultPath;
|
||||||
|
const hasFilePath = Boolean(currentPath);
|
||||||
|
|
||||||
|
const loadHeartbeatFile = useCallback(async (path: string) => {
|
||||||
|
setIsLoadingFile(true);
|
||||||
|
setFileLoadError(null);
|
||||||
|
try {
|
||||||
|
const data = await fetchWorkspaceFileContent("project", path, projectId);
|
||||||
|
setFileContent(data.content);
|
||||||
|
setFileContentDirty(false);
|
||||||
|
} catch (err) {
|
||||||
|
const message = getErrorMessage(err);
|
||||||
|
setFileLoadError(message);
|
||||||
|
addToast(`Failed to load heartbeat procedure file: ${message}`, "error");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingFile(false);
|
||||||
|
}
|
||||||
|
}, [addToast, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowFileViewer(false);
|
||||||
|
setShowPreview(false);
|
||||||
|
setFileContent("");
|
||||||
|
setFileContentDirty(false);
|
||||||
|
setFileLoadError(null);
|
||||||
|
setIsLoadingFile(false);
|
||||||
|
setIsSavingFile(false);
|
||||||
|
setJustSavedFile(false);
|
||||||
|
}, [agent.id, currentPath]);
|
||||||
|
|
||||||
|
const handleOpenViewer = async () => {
|
||||||
|
if (!currentPath) return;
|
||||||
|
setShowFileViewer(true);
|
||||||
|
await loadHeartbeatFile(currentPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveFile = async () => {
|
||||||
|
if (!currentPath) return;
|
||||||
|
setIsSavingFile(true);
|
||||||
|
try {
|
||||||
|
await saveWorkspaceFileContent("project", currentPath, fileContent, projectId);
|
||||||
|
setFileContentDirty(false);
|
||||||
|
setJustSavedFile(true);
|
||||||
|
addToast("Heartbeat procedure file saved", "success");
|
||||||
|
setTimeout(() => setJustSavedFile(false), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
addToast(`Failed to save heartbeat procedure file: ${getErrorMessage(err)}`, "error");
|
||||||
|
} finally {
|
||||||
|
setIsSavingFile(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpgrade = async () => {
|
const handleUpgrade = async () => {
|
||||||
setIsUpgrading(true);
|
setIsUpgrading(true);
|
||||||
@@ -2556,6 +2614,27 @@ function HeartbeatProcedureSection({
|
|||||||
<span className="config-hint">
|
<span className="config-hint">
|
||||||
Current path: <code>{currentPath || "(none — using built-in default)"}</code>
|
Current path: <code>{currentPath || "(none — using built-in default)"}</code>
|
||||||
</span>
|
</span>
|
||||||
|
{hasFilePath && (
|
||||||
|
<div className="heartbeat-procedure-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => void handleOpenViewer()}
|
||||||
|
disabled={isLoadingFile}
|
||||||
|
>
|
||||||
|
{isLoadingFile ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
Loading file…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FileText size={16} />
|
||||||
|
View Heartbeat Markdown
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="config-field">
|
<div className="config-field">
|
||||||
<button
|
<button
|
||||||
@@ -2587,6 +2666,105 @@ function HeartbeatProcedureSection({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showFileViewer && hasFilePath && currentPath && (
|
||||||
|
<div className="config-fields heartbeat-procedure-viewer">
|
||||||
|
<div className="config-field">
|
||||||
|
<label htmlFor="heartbeat-procedure-file-content">Heartbeat Procedure File</label>
|
||||||
|
<div className="agent-content-toolbar">
|
||||||
|
<div className="agent-content-mode-toggle">
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${!showPreview ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => setShowPreview(false)}
|
||||||
|
disabled={!showPreview}
|
||||||
|
aria-label="Heartbeat file edit mode"
|
||||||
|
>
|
||||||
|
<FileEdit size={14} />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn btn-sm ${showPreview ? "btn-primary" : ""}`}
|
||||||
|
onClick={() => setShowPreview(true)}
|
||||||
|
disabled={showPreview}
|
||||||
|
aria-label="Heartbeat file preview mode"
|
||||||
|
>
|
||||||
|
<Eye size={14} />
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{isLoadingFile && (
|
||||||
|
<span className="config-hint heartbeat-procedure-status">
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{fileContentDirty && !isLoadingFile && (
|
||||||
|
<span className="config-hint heartbeat-procedure-status heartbeat-procedure-status--warning">
|
||||||
|
Unsaved changes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showPreview ? (
|
||||||
|
fileContent.trim() ? (
|
||||||
|
<div className="agent-content-preview markdown-body">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{fileContent}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="agent-content-preview agent-content-placeholder">
|
||||||
|
No heartbeat procedure markdown content yet.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<textarea
|
||||||
|
id="heartbeat-procedure-file-content"
|
||||||
|
className="input"
|
||||||
|
rows={16}
|
||||||
|
value={fileContent}
|
||||||
|
readOnly={isLoadingFile}
|
||||||
|
placeholder="Heartbeat procedure markdown file content will appear here..."
|
||||||
|
onChange={(e) => {
|
||||||
|
setFileContent(e.target.value);
|
||||||
|
setFileContentDirty(true);
|
||||||
|
setJustSavedFile(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{fileLoadError && (
|
||||||
|
<span className="config-error">Failed to load file: {fileLoadError}</span>
|
||||||
|
)}
|
||||||
|
<span className="config-hint">
|
||||||
|
This editor writes directly to <code>{currentPath}</code>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!showPreview && (
|
||||||
|
<div className="config-actions">
|
||||||
|
<button
|
||||||
|
className="btn btn-task-create"
|
||||||
|
disabled={!fileContentDirty || isSavingFile || isLoadingFile}
|
||||||
|
onClick={() => void handleSaveFile()}
|
||||||
|
>
|
||||||
|
{isSavingFile ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
Saving…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle size={16} />
|
||||||
|
Save Heartbeat File
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{!fileContentDirty && justSavedFile && (
|
||||||
|
<span className="config-saved-indicator">
|
||||||
|
<CheckCircle size={14} />
|
||||||
|
File saved
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ vi.mock("../../api", () => ({
|
|||||||
fetchAgentRuns: vi.fn(),
|
fetchAgentRuns: vi.fn(),
|
||||||
fetchAgentRunDetail: vi.fn(),
|
fetchAgentRunDetail: vi.fn(),
|
||||||
startAgentRun: vi.fn(),
|
startAgentRun: vi.fn(),
|
||||||
|
stopAgentRun: vi.fn(),
|
||||||
updateAgentInstructions: vi.fn(),
|
updateAgentInstructions: vi.fn(),
|
||||||
updateAgentSoul: vi.fn(),
|
updateAgentSoul: vi.fn(),
|
||||||
updateAgentMemory: vi.fn(),
|
updateAgentMemory: vi.fn(),
|
||||||
@@ -34,6 +35,7 @@ vi.mock("../../api", () => ({
|
|||||||
fetchDiscoveredSkills: vi.fn(),
|
fetchDiscoveredSkills: vi.fn(),
|
||||||
fetchModels: vi.fn(),
|
fetchModels: vi.fn(),
|
||||||
fetchPluginRuntimes: vi.fn(),
|
fetchPluginRuntimes: vi.fn(),
|
||||||
|
upgradeAgentHeartbeatProcedure: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../AgentLogViewer", () => ({
|
vi.mock("../AgentLogViewer", () => ({
|
||||||
@@ -97,7 +99,7 @@ vi.mock("../../hooks/useConfirm", () => ({
|
|||||||
useConfirm: () => ({ confirm: mockConfirm }),
|
useConfirm: () => ({ confirm: mockConfirm }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { fetchAgent, fetchAgents, updateAgent, updateAgentState, deleteAgent, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchDiscoveredSkills, fetchModels, fetchPluginRuntimes, fetchAgentLogsWithMeta } from "../../api";
|
import { fetchAgent, fetchAgents, updateAgent, updateAgentState, deleteAgent, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchDiscoveredSkills, fetchModels, fetchPluginRuntimes, fetchAgentLogsWithMeta, upgradeAgentHeartbeatProcedure } from "../../api";
|
||||||
|
|
||||||
const mockFetchAgent = vi.mocked(fetchAgent);
|
const mockFetchAgent = vi.mocked(fetchAgent);
|
||||||
const mockFetchAgents = vi.mocked(fetchAgents);
|
const mockFetchAgents = vi.mocked(fetchAgents);
|
||||||
@@ -121,6 +123,7 @@ const mockFetchDiscoveredSkills = vi.mocked(fetchDiscoveredSkills);
|
|||||||
const mockFetchModels = vi.mocked(fetchModels);
|
const mockFetchModels = vi.mocked(fetchModels);
|
||||||
const mockFetchPluginRuntimes = vi.mocked(fetchPluginRuntimes);
|
const mockFetchPluginRuntimes = vi.mocked(fetchPluginRuntimes);
|
||||||
const mockFetchAgentLogsWithMeta = vi.mocked(fetchAgentLogsWithMeta);
|
const mockFetchAgentLogsWithMeta = vi.mocked(fetchAgentLogsWithMeta);
|
||||||
|
const mockUpgradeAgentHeartbeatProcedure = vi.mocked(upgradeAgentHeartbeatProcedure);
|
||||||
|
|
||||||
const MOCK_SKILLS = [
|
const MOCK_SKILLS = [
|
||||||
{ id: "skill-1", name: "Skill One", path: "/path/skill-1", relativePath: "skills/skill-1", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
{ id: "skill-1", name: "Skill One", path: "/path/skill-1", relativePath: "skills/skill-1", enabled: true, metadata: { source: "*", scope: "user" as const, origin: "top-level" as const } },
|
||||||
@@ -214,6 +217,10 @@ describe("AgentDetailView", () => {
|
|||||||
{ pluginId: "fusion-plugin-openclaw-runtime", runtimeId: "openclaw", name: "OpenClaw", description: "OpenClaw runtime", version: "1.0.0" },
|
{ pluginId: "fusion-plugin-openclaw-runtime", runtimeId: "openclaw", name: "OpenClaw", description: "OpenClaw runtime", version: "1.0.0" },
|
||||||
{ pluginId: "fusion-plugin-hermes-runtime", runtimeId: "hermes", name: "Hermes", description: "Hermes runtime", version: "1.1.0" },
|
{ pluginId: "fusion-plugin-hermes-runtime", runtimeId: "hermes", name: "Hermes", description: "Hermes runtime", version: "1.1.0" },
|
||||||
]);
|
]);
|
||||||
|
mockUpgradeAgentHeartbeatProcedure.mockResolvedValue({
|
||||||
|
heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md",
|
||||||
|
procedureFileSeeded: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows loading state initially", () => {
|
it("shows loading state initially", () => {
|
||||||
@@ -3856,4 +3863,85 @@ describe("AgentDetailView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Heartbeat procedure file viewer", () => {
|
||||||
|
const openSettings = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||||
|
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(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
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(<AgentDetailView agentId="agent-001" projectId="proj-1" onClose={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
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(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={addToast} />);
|
||||||
|
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(<AgentDetailView agentId="agent-001" projectId="proj-2" onClose={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ vi.mock("../../api", () => ({
|
|||||||
cancelAgentGeneration: vi.fn(),
|
cancelAgentGeneration: vi.fn(),
|
||||||
fetchAgentBudgetStatus: vi.fn(),
|
fetchAgentBudgetStatus: vi.fn(),
|
||||||
resetAgentBudget: vi.fn(),
|
resetAgentBudget: vi.fn(),
|
||||||
|
upgradeAgentHeartbeatProcedure: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../AgentLogViewer", () => ({
|
vi.mock("../AgentLogViewer", () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user