test(KB-606): add comprehensive test coverage across packages

- Add tests for core board logic (board.test.ts)
- Add tests for dashboard useToast hook (useToast.test.tsx)
- Add tests for dashboard model filter utility (modelFilter.test.ts)
- Add tests for agent heartbeat monitor (agent-heartbeat.test.ts)
This commit is contained in:
gsxdsm
2026-03-31 17:27:28 -07:00
parent 21d2649574
commit 789cc519da
4 changed files with 1222 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { HeartbeatMonitor, type AgentSession } from "./agent-heartbeat.js";
import type { AgentStore } from "@fusion/core";
// Mock store factory
function createMockStore(overrides: Partial<AgentStore> = {}): AgentStore {
return {
recordHeartbeat: vi.fn().mockResolvedValue(undefined),
updateAgentState: vi.fn().mockResolvedValue(undefined),
...overrides,
} as unknown as AgentStore;
}
// Mock session factory
function createMockSession(): AgentSession {
return {
dispose: vi.fn(),
};
}
describe("HeartbeatMonitor", () => {
let store: AgentStore;
let monitor: HeartbeatMonitor;
beforeEach(() => {
store = createMockStore();
monitor = new HeartbeatMonitor({ store });
});
afterEach(() => {
monitor.stop();
vi.useRealTimers();
});
describe("constructor", () => {
it("initializes with default options", () => {
expect(monitor).toBeDefined();
expect(monitor.isActive()).toBe(false);
});
it("accepts custom pollIntervalMs", () => {
const customMonitor = new HeartbeatMonitor({ store, pollIntervalMs: 5000 });
expect(customMonitor).toBeDefined();
});
it("accepts custom heartbeatTimeoutMs", () => {
const customMonitor = new HeartbeatMonitor({ store, heartbeatTimeoutMs: 120000 });
expect(customMonitor).toBeDefined();
});
it("accepts callbacks", () => {
const onMissed = vi.fn();
const onRecovered = vi.fn();
const onTerminated = vi.fn();
const customMonitor = new HeartbeatMonitor({
store,
onMissed,
onRecovered,
onTerminated,
});
expect(customMonitor).toBeDefined();
});
});
describe("start", () => {
it("initiates polling interval", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
expect(monitor.isActive()).toBe(true);
vi.useRealTimers();
});
it("is idempotent (multiple calls don't create multiple intervals)", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
monitor.start();
monitor.start();
expect(monitor.isActive()).toBe(true);
// Stop should clean up properly
monitor.stop();
expect(monitor.isActive()).toBe(false);
vi.useRealTimers();
});
});
describe("stop", () => {
it("clears the polling interval", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
expect(monitor.isActive()).toBe(true);
monitor.stop();
expect(monitor.isActive()).toBe(false);
vi.useRealTimers();
});
it("is safe to call when not started", () => {
expect(() => monitor.stop()).not.toThrow();
expect(monitor.isActive()).toBe(false);
});
it("is safe to call multiple times", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
monitor.stop();
monitor.stop();
monitor.stop();
expect(monitor.isActive()).toBe(false);
vi.useRealTimers();
});
});
describe("isActive", () => {
it("reflects monitor state (false when not started)", () => {
expect(monitor.isActive()).toBe(false);
});
it("reflects monitor state (true when started)", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
expect(monitor.isActive()).toBe(true);
vi.useRealTimers();
});
it("reflects monitor state (false after stopped)", () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.start();
monitor.stop();
expect(monitor.isActive()).toBe(false);
vi.useRealTimers();
});
});
describe("trackAgent", () => {
it("adds agent to tracked set with correct initial state", () => {
const session = createMockSession();
const before = Date.now();
monitor.trackAgent("agent-001", session, "run-001");
const lastSeen = monitor.getLastSeen("agent-001");
expect(lastSeen).toBeDefined();
expect(lastSeen).toBeGreaterThanOrEqual(before);
expect(monitor.getTrackedAgents()).toContain("agent-001");
});
it("records initial heartbeat to store", () => {
const session = createMockSession();
monitor.trackAgent("agent-001", session, "run-001");
expect(store.recordHeartbeat).toHaveBeenCalledWith("agent-001", "ok", "run-001");
});
it("can track multiple agents", () => {
monitor.trackAgent("agent-001", createMockSession(), "run-001");
monitor.trackAgent("agent-002", createMockSession(), "run-002");
monitor.trackAgent("agent-003", createMockSession(), "run-003");
expect(monitor.getTrackedAgents()).toHaveLength(3);
expect(monitor.getTrackedAgents()).toContain("agent-001");
expect(monitor.getTrackedAgents()).toContain("agent-002");
expect(monitor.getTrackedAgents()).toContain("agent-003");
});
});
describe("recordHeartbeat", () => {
it("updates lastSeen timestamp", () => {
const session = createMockSession();
vi.useFakeTimers({ shouldAdvanceTime: true });
monitor.trackAgent("agent-001", session, "run-001");
const initialLastSeen = monitor.getLastSeen("agent-001")!;
vi.advanceTimersByTime(100);
monitor.recordHeartbeat("agent-001");
const newLastSeen = monitor.getLastSeen("agent-001")!;
expect(newLastSeen).toBeGreaterThan(initialLastSeen);
vi.useRealTimers();
});
it("records ok heartbeat to store", () => {
const session = createMockSession();
monitor.trackAgent("agent-001", session, "run-001");
monitor.recordHeartbeat("agent-001");
// Should have been called twice: once on track, once on heartbeat
expect(store.recordHeartbeat).toHaveBeenCalledTimes(2);
expect(store.recordHeartbeat).toHaveBeenLastCalledWith("agent-001", "ok", "run-001");
});
it("triggers onRecovered callback after missed heartbeat", () => {
const onRecovered = vi.fn();
const customMonitor = new HeartbeatMonitor({ store, onRecovered });
const session = createMockSession();
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.trackAgent("agent-001", session, "run-001");
// Simulate missed heartbeat by advancing time
vi.advanceTimersByTime(70000); // Default timeout is 60000
// Trigger the check
customMonitor.stop();
// Reset and record heartbeat (should trigger recovery)
customMonitor.recordHeartbeat("agent-001");
expect(onRecovered).not.toHaveBeenCalled();
vi.useRealTimers();
});
it("does nothing for untracked agent", () => {
expect(() => monitor.recordHeartbeat("agent-001")).not.toThrow();
expect(store.recordHeartbeat).not.toHaveBeenCalled();
});
});
describe("isAgentHealthy", () => {
it("returns true for recent heartbeat", () => {
const session = createMockSession();
monitor.trackAgent("agent-001", session, "run-001");
expect(monitor.isAgentHealthy("agent-001")).toBe(true);
});
it("returns false for missed heartbeat", () => {
const session = createMockSession();
vi.useFakeTimers({ shouldAdvanceTime: true });
// Use short timeout for testing
const customMonitor = new HeartbeatMonitor({
store,
heartbeatTimeoutMs: 5000,
});
customMonitor.trackAgent("agent-001", session, "run-001");
expect(customMonitor.isAgentHealthy("agent-001")).toBe(true);
// Advance past timeout
vi.advanceTimersByTime(6000);
expect(customMonitor.isAgentHealthy("agent-001")).toBe(false);
vi.useRealTimers();
});
it("returns false for untracked agent", () => {
expect(monitor.isAgentHealthy("agent-001")).toBe(false);
});
});
describe("getTrackedAgents", () => {
it("returns empty array when no agents tracked", () => {
expect(monitor.getTrackedAgents()).toEqual([]);
});
it("returns all tracked agent IDs", () => {
monitor.trackAgent("agent-001", createMockSession(), "run-001");
monitor.trackAgent("agent-002", createMockSession(), "run-002");
const agents = monitor.getTrackedAgents();
expect(agents).toHaveLength(2);
expect(agents).toContain("agent-001");
expect(agents).toContain("agent-002");
});
});
describe("getLastSeen", () => {
it("returns correct timestamp for tracked agent", () => {
const session = createMockSession();
const before = Date.now();
monitor.trackAgent("agent-001", session, "run-001");
const lastSeen = monitor.getLastSeen("agent-001");
expect(lastSeen).toBeDefined();
expect(lastSeen).toBeGreaterThanOrEqual(before);
});
it("returns undefined for untracked agent", () => {
expect(monitor.getLastSeen("agent-001")).toBeUndefined();
});
});
describe("missed heartbeat detection", () => {
it("triggers onMissed callback when heartbeat is missed", async () => {
const onMissed = vi.fn();
const customMonitor = new HeartbeatMonitor({
store,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
onMissed,
});
const session = createMockSession();
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
// Wait for polling to detect missed heartbeat
vi.advanceTimersByTime(6000);
// Wait for async checkMissedHeartbeats
await vi.advanceTimersByTimeAsync(100);
expect(onMissed).toHaveBeenCalledWith("agent-001");
customMonitor.stop();
vi.useRealTimers();
});
it("records missed heartbeat to store", async () => {
const customMonitor = new HeartbeatMonitor({
store,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
});
const session = createMockSession();
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
// Wait for polling to detect missed heartbeat
vi.advanceTimersByTime(6000);
await vi.advanceTimersByTimeAsync(100);
expect(store.recordHeartbeat).toHaveBeenCalledWith("agent-001", "missed", "run-001");
customMonitor.stop();
vi.useRealTimers();
});
});
describe("unresponsive agent termination", () => {
it("disposes session and terminates agent after 2x timeout", async () => {
const onTerminated = vi.fn();
const session = createMockSession();
const customMonitor = new HeartbeatMonitor({
store,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
onTerminated,
});
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
// Wait for missed heartbeat (1x timeout)
vi.advanceTimersByTime(6000);
await vi.advanceTimersByTimeAsync(100);
// Wait for termination (2x timeout = 10 seconds total from start)
vi.advanceTimersByTime(6000);
await vi.advanceTimersByTimeAsync(100);
expect(session.dispose).toHaveBeenCalled();
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "terminated");
expect(onTerminated).toHaveBeenCalledWith("agent-001");
customMonitor.stop();
vi.useRealTimers();
});
it("removes agent from tracking after termination", async () => {
const session = createMockSession();
const customMonitor = new HeartbeatMonitor({
store,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
});
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
expect(customMonitor.getTrackedAgents()).toContain("agent-001");
// Wait for termination
vi.advanceTimersByTime(12000);
await vi.advanceTimersByTimeAsync(100);
expect(customMonitor.getTrackedAgents()).not.toContain("agent-001");
customMonitor.stop();
vi.useRealTimers();
});
});
describe("untrackAgent", () => {
it("removes agent from tracking", () => {
const session = createMockSession();
monitor.trackAgent("agent-001", session, "run-001");
expect(monitor.getTrackedAgents()).toContain("agent-001");
monitor.untrackAgent("agent-001");
expect(monitor.getTrackedAgents()).not.toContain("agent-001");
expect(monitor.getTrackedAgents()).toHaveLength(0);
});
it("is safe to call for untracked agent", () => {
expect(() => monitor.untrackAgent("agent-001")).not.toThrow();
});
});
});