fix(FN-2198): sync agent heartbeat config UI with live runtime updates

- Refresh AgentDetailView on agent:updated SSE events while preserving local unsaved config edits
- Resync heartbeat and budget form state from latest runtime config when agent data changes
- Centralize heartbeat interval defaults/formatting in shared utilities and reuse them in AgentsView selectors
- Add dashboard tests for default heartbeat hints, unset runtime fallback behavior, and custom interval options
This commit is contained in:
Fusion
2026-04-20 14:36:18 -07:00
committed by gsxdsm
parent 238c5e27ec
commit db8258f8bd
5 changed files with 234 additions and 79 deletions

View File

@@ -4,6 +4,7 @@ import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import { AgentDetailView } from "../AgentDetailView";
import type { AgentCapability, AgentDetail } from "../../api";
import { DEFAULT_HEARTBEAT_INTERVAL_MS } from "../../utils/heartbeatIntervals";
// Mock the API functions
vi.mock("../../api", () => ({
@@ -1122,6 +1123,27 @@ describe("AgentDetailView", () => {
});
});
it("shows shared system default hint for heartbeat interval", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} }));
const user = userEvent.setup();
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
await navigateToSettings(user);
const heartbeatInput = await screen.findByLabelText("Heartbeat Interval (ms)");
expect(heartbeatInput).toHaveAttribute("placeholder", String(DEFAULT_HEARTBEAT_INTERVAL_MS));
expect(
screen.getByText(`How often heartbeats are checked. Leave empty for system default (${DEFAULT_HEARTBEAT_INTERVAL_MS}ms / 1h).`),
).toBeInTheDocument();
});
it("pre-fills heartbeat fields from agent runtimeConfig", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({
runtimeConfig: {

View File

@@ -181,6 +181,25 @@ describe("AgentsView", () => {
expect(screen.getByDisplayValue("30s")).toBeTruthy();
});
it("uses the system default heartbeat interval when runtime config is unset", async () => {
mockFetchAgents.mockResolvedValue([
{
...mockAgents[1],
runtimeConfig: {},
},
]);
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByLabelText("Set heartbeat interval for Test Agent 2")).toBeTruthy();
});
const intervalSelect = screen.getByLabelText("Set heartbeat interval for Test Agent 2") as HTMLSelectElement;
expect(intervalSelect.value).toBe("3600000");
expect(intervalSelect.options[intervalSelect.selectedIndex]?.text).toBe("1h");
});
it("updates agent heartbeat interval from preset dropdown", async () => {
render(<AgentsView addToast={mockAddToast} />);
@@ -202,7 +221,7 @@ describe("AgentsView", () => {
});
});
it("maps non-preset heartbeat interval to closest preset", async () => {
it("shows a custom heartbeat option when configured interval is not a preset", async () => {
mockFetchAgents.mockResolvedValue([
{
...mockAgents[1],
@@ -217,8 +236,9 @@ describe("AgentsView", () => {
});
const intervalSelect = screen.getByLabelText("Set heartbeat interval for Test Agent 2") as HTMLSelectElement;
expect(intervalSelect.value).toBe("60000");
expect(screen.getAllByText("1m").length).toBeGreaterThan(0);
expect(intervalSelect.value).toBe("65000");
expect(intervalSelect.options[intervalSelect.selectedIndex]?.text).toBe("1m (custom)");
expect(screen.getByRole("option", { name: "1m (custom)" })).toBeTruthy();
});
it("shows refresh button", async () => {