feat(FN-2099): merge fusion/fn-2099

This commit is contained in:
gsxdsm
2026-04-19 02:07:08 -07:00
parent af104006ba
commit a78eaa21bd
2 changed files with 106 additions and 2 deletions

View File

@@ -34,6 +34,7 @@ vi.mock("./pi.js", () => ({
// Import the mocked functions for test control
import { createKbAgent } from "./pi.js";
import { heartbeatLog } from "./logger.js";
const mockedCreateKbAgent = vi.mocked(createKbAgent);
// Mock store factory
@@ -721,6 +722,109 @@ describe("HeartbeatMonitor", () => {
customMonitor.stop();
vi.useRealTimers();
});
it("logs warning when session dispose throws during termination", async () => {
const warnSpy = vi.mocked(heartbeatLog.warn);
warnSpy.mockClear();
const session: AgentSession = {
dispose: vi.fn(() => {
throw new Error("dispose exploded");
}),
};
const updateAgentState = vi.fn().mockResolvedValue(undefined);
const localStore = createMockStore({ updateAgentState });
const onTerminated = vi.fn();
const customMonitor = new HeartbeatMonitor({
store: localStore,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
onTerminated,
});
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
vi.advanceTimersByTime(10100);
await vi.advanceTimersByTimeAsync(100);
const warnMessages = warnSpy.mock.calls.map(([message]) => String(message));
expect(warnMessages.some((message) => message.includes("Error disposing session for agent-001") && message.includes("dispose exploded"))).toBe(true);
expect(updateAgentState).toHaveBeenCalledWith("agent-001", "terminated");
expect(onTerminated).toHaveBeenCalledWith("agent-001");
expect(customMonitor.getTrackedAgents()).toHaveLength(0);
customMonitor.stop();
vi.useRealTimers();
});
it("logs warning when updateAgentState throws during termination", async () => {
const warnSpy = vi.mocked(heartbeatLog.warn);
warnSpy.mockClear();
const session = createMockSession();
const localStore = createMockStore({
updateAgentState: vi.fn().mockRejectedValue(new Error("db connection lost")),
});
const onTerminated = vi.fn();
const customMonitor = new HeartbeatMonitor({
store: localStore,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
onTerminated,
});
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
vi.advanceTimersByTime(10100);
await vi.advanceTimersByTimeAsync(100);
const warnMessages = warnSpy.mock.calls.map(([message]) => String(message));
expect(warnMessages.some((message) => message.includes("Error terminating agent agent-001") && message.includes("db connection lost"))).toBe(true);
expect(onTerminated).toHaveBeenCalledWith("agent-001");
expect(customMonitor.getTrackedAgents()).toHaveLength(0);
customMonitor.stop();
vi.useRealTimers();
});
it("logs warnings from both dispose and state update when both fail", async () => {
const warnSpy = vi.mocked(heartbeatLog.warn);
warnSpy.mockClear();
const session: AgentSession = {
dispose: vi.fn(() => {
throw new Error("dispose exploded");
}),
};
const localStore = createMockStore({
updateAgentState: vi.fn().mockRejectedValue(new Error("db connection lost")),
});
const onTerminated = vi.fn();
const customMonitor = new HeartbeatMonitor({
store: localStore,
heartbeatTimeoutMs: 5000,
pollIntervalMs: 1000,
onTerminated,
});
vi.useFakeTimers({ shouldAdvanceTime: true });
customMonitor.start();
customMonitor.trackAgent("agent-001", session, "run-001");
vi.advanceTimersByTime(10100);
await vi.advanceTimersByTimeAsync(100);
const warnMessages = warnSpy.mock.calls.map(([message]) => String(message));
expect(warnMessages).toHaveLength(2);
expect(warnMessages.some((message) => message.includes("Error disposing session for agent-001") && message.includes("dispose exploded"))).toBe(true);
expect(warnMessages.some((message) => message.includes("Error terminating agent agent-001") && message.includes("db connection lost"))).toBe(true);
expect(onTerminated).toHaveBeenCalledWith("agent-001");
expect(customMonitor.getTrackedAgents()).toHaveLength(0);
customMonitor.stop();
vi.useRealTimers();
});
});
describe("untrackAgent", () => {

View File

@@ -1567,14 +1567,14 @@ export class HeartbeatMonitor {
tracked.session.dispose();
} catch (err) {
// Log but don't stop termination
console.error(`[HeartbeatMonitor] Error disposing session for ${tracked.agentId}:`, err);
heartbeatLog.warn(`Error disposing session for ${tracked.agentId}: ${err instanceof Error ? err.message : String(err)}`);
}
// Update agent state to terminated
try {
await this.store.updateAgentState(tracked.agentId, "terminated");
} catch (err) {
console.error(`[HeartbeatMonitor] Error terminating agent ${tracked.agentId}:`, err);
heartbeatLog.warn(`Error terminating agent ${tracked.agentId}: ${err instanceof Error ? err.message : String(err)}`);
}
// Remove from tracking