Files
fusion/packages/engine/src/__tests__/heartbeat-executor.test.ts
Fusion 631b384837 feat(FN-4005): harden durable transient recovery auto-restart flow
- Add bounded auto-restart handling in in-process runtime for durable agents after transient failures
- Extend self-healing recovery logic and transient error detection to classify and recover retryable runtime interruptions
- Add regression coverage across heartbeat executor, self-healing, and transient detector test suites
- Document durable agent transient recovery behavior in docs/agents.md

Fusion-Task-Id: FN-4005
2026-05-11 09:17:10 -07:00

3133 lines
134 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
HeartbeatMonitor,
HEARTBEAT_SYSTEM_PROMPT,
HEARTBEAT_NO_TASK_SYSTEM_PROMPT,
HEARTBEAT_PROCEDURE,
HEARTBEAT_NO_TASK_PROCEDURE,
} from "../agent-heartbeat.js";
import { AgentLogger } from "../agent-logger.js";
import type { AgentStore, AgentHeartbeatRun, TaskStore, TaskDetail, Agent, MessageStore, Message } from "@fusion/core";
import { createMessage, createBudgetStatus } from "./heartbeat-test-helpers.js";
vi.mock("../logger.js", async () => {
const { createMockLogger, formatMockError } = await import("./heartbeat-test-helpers.js");
return {
createLogger: vi.fn(() => createMockLogger()),
heartbeatLog: createMockLogger(),
formatError: formatMockError,
};
});
vi.mock("../pi.js", () => ({
createFnAgent: vi.fn(),
promptWithFallback: vi.fn(async (session: any, prompt: string) => {
await session.prompt(prompt);
}),
}));
import { createFnAgent } from "../pi.js";
const mockedCreateFnAgent = vi.mocked(createFnAgent);
describe("executeHeartbeat", () => {
let mockTaskStore: TaskStore;
let mockAgent: Agent;
// Helper: create a mock session returned by createFnAgent
function createMockAgentSession() {
return {
prompt: vi.fn().mockResolvedValue(undefined),
dispose: vi.fn(),
subscribe: vi.fn(),
model: { provider: "mock", id: "mock-model" },
};
}
type MockTaskStoreOverrides = Partial<TaskStore> & {
checkoutTask?: (taskId: string, agentId: string) => Promise<unknown>;
};
// Helper: create a basic mock task store
function createMockTaskStore(overrides: MockTaskStoreOverrides = {}): TaskStore {
return {
getTask: vi.fn().mockResolvedValue({
id: "FN-001",
title: "Test Task",
description: "Test task description",
prompt: "# Test PROMPT.md\nSome content",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
selectNextTaskForAgent: vi.fn().mockResolvedValue(null),
listTasks: vi.fn().mockResolvedValue([]),
createTask: vi.fn().mockResolvedValue({
id: "FN-002",
description: "Created task",
dependencies: [],
column: "triage",
}),
logEntry: vi.fn().mockResolvedValue({}),
addComment: vi.fn().mockResolvedValue({}),
appendAgentLog: vi.fn().mockResolvedValue(undefined),
// Document-related methods for task_document tools
upsertTaskDocument: vi.fn().mockResolvedValue({
id: "doc-1",
taskId: "FN-001",
key: "test-plan",
content: "Test document content",
revision: 1,
author: "agent",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
getTaskDocument: vi.fn().mockResolvedValue({
id: "doc-1",
taskId: "FN-001",
key: "test-plan",
content: "Test document content",
revision: 1,
author: "agent",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
getTaskDocuments: vi.fn().mockResolvedValue([]),
...overrides,
} as unknown as TaskStore;
}
// Helper: create a mock store that returns a specific agent
function createStoreWithAgentForExec(agentData: Partial<Agent> = {}): AgentStore {
mockAgent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "active",
taskId: "FN-001",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
...agentData,
} as Agent;
// Track saved runs so getRunDetail returns the most recent state
const savedRuns: Map<string, AgentHeartbeatRun> = new Map();
return {
recordHeartbeat: vi.fn().mockResolvedValue(undefined),
updateAgentState: vi.fn().mockResolvedValue(undefined),
updateAgent: vi.fn().mockResolvedValue(undefined),
getAgent: vi.fn().mockResolvedValue(mockAgent),
assignTask: vi.fn().mockImplementation(async (_agentId: string, taskId: string | undefined) => {
mockAgent.taskId = taskId;
return mockAgent;
}),
claimTaskForAgent: vi.fn().mockImplementation(async (_agentId: string, _taskId: string) => ({
ok: false,
reason: "task_not_found",
})),
startHeartbeatRun: vi.fn().mockResolvedValue({
id: "run-001",
agentId: "agent-001",
startedAt: new Date().toISOString(),
endedAt: null,
status: "active",
} as AgentHeartbeatRun),
saveRun: vi.fn().mockImplementation(async (run: AgentHeartbeatRun) => {
savedRuns.set(run.id, run);
}),
getRunDetail: vi.fn().mockImplementation(async (_agentId: string, runId: string) => {
return savedRuns.get(runId) ?? {
id: runId,
agentId: "agent-001",
startedAt: new Date().toISOString(),
endedAt: new Date().toISOString(),
status: "completed" as const,
};
}),
getRatingSummary: vi.fn().mockResolvedValue(undefined),
endHeartbeatRun: vi.fn().mockResolvedValue(undefined),
getBudgetStatus: vi.fn().mockResolvedValue(createBudgetStatus()),
getCachedAgent: vi.fn().mockReturnValue(null),
getLastBlockedState: vi.fn().mockResolvedValue(null),
setLastBlockedState: vi.fn().mockResolvedValue(undefined),
clearLastBlockedState: vi.fn().mockResolvedValue(undefined),
appendRunLog: vi.fn().mockResolvedValue(undefined),
getAgentsByReportsTo: vi.fn().mockResolvedValue([]),
} as unknown as AgentStore;
}
beforeEach(() => {
mockTaskStore = createMockTaskStore();
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("reports health check", () => {
it("buildReportsHealthSection returns null when agent has no reports", async () => {
const store = createStoreWithAgentForExec();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await (monitor as any).buildReportsHealthSection("agent-001", store);
expect(result).toBeNull();
});
it("buildReportsHealthSection returns formatted table for healthy reports", async () => {
const now = new Date().toISOString();
const store = createStoreWithAgentForExec();
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-002", name: "agent-2", state: "active", taskId: "FN-100", lastHeartbeatAt: now, updatedAt: now } as Agent,
{ id: "agent-003", name: "agent-3", state: "running", taskId: "FN-101", lastHeartbeatAt: now, updatedAt: now } as Agent,
]);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const section = await (monitor as any).buildReportsHealthSection("agent-001", store);
expect(section).toContain("## Reports Health Check");
expect(section).toContain("agent-2");
expect(section).toContain("agent-3");
expect(section).toContain("| Name | State | Task | Last Heartbeat | Health |");
expect(section).toContain("healthy");
});
it("buildReportsHealthSection classifies stuck agents", async () => {
const now = Date.now();
const store = createStoreWithAgentForExec();
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-002", name: "agent-2", state: "error", taskId: "FN-100", lastHeartbeatAt: new Date(now - 1000).toISOString(), updatedAt: new Date(now - 1000).toISOString(), lastError: "boom" } as Agent,
]);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp", heartbeatTimeoutMs: 60_000 });
const section = await (monitor as any).buildReportsHealthSection("agent-001", store);
expect(section).toContain("**stuck**");
expect(section).toContain("Actions for Unresponsive Reports");
});
it("buildReportsHealthSection classifies stale agents", async () => {
const now = Date.now();
const store = createStoreWithAgentForExec();
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-003", name: "agent-3", state: "active", taskId: "FN-101", lastHeartbeatAt: new Date(now - 3 * 60 * 60 * 1000).toISOString(), updatedAt: new Date(now - 3 * 60 * 60 * 1000).toISOString() } as Agent,
]);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp", heartbeatTimeoutMs: 60_000 });
const section = await (monitor as any).buildReportsHealthSection("agent-001", store);
expect(section).toContain("**stale**");
});
it("executeHeartbeat includes reports health section when agent has reports", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const now = new Date().toISOString();
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([
{ id: "agent-010", name: "reporter", state: "running", taskId: "FN-200", lastHeartbeatAt: now, updatedAt: now } as Agent,
]);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
expect(executionPrompt).toContain("## Reports Health Check");
expect(executionPrompt).toContain("reporter");
});
it("executeHeartbeat omits reports health section when agent has no reports", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
vi.mocked(store.getAgentsByReportsTo).mockResolvedValue([]);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
expect(executionPrompt).not.toContain("## Reports Health Check");
});
});
it("passes action gate and permanent gating context for permanent heartbeat agents", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const args = mockedCreateFnAgent.mock.calls[0]?.[0] as {
actionGateContext?: { agentId: string; isEphemeral: boolean };
permanentAgentGating?: { permissionPolicy?: { presetId: string } };
};
expect(args.actionGateContext?.agentId).toBe("agent-001");
expect(args.actionGateContext?.isEphemeral).toBe(false);
expect(args.permanentAgentGating?.permissionPolicy?.presetId).toBe("unrestricted");
});
it("pauseForApproval pauses task and agent when taskId exists", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const pauseTask = vi.fn().mockResolvedValue(undefined);
const logEntry = vi.fn().mockResolvedValue(undefined);
mockTaskStore = createMockTaskStore({ pauseTask, logEntry });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const ctx = (monitor as any).buildActionGateContext({ id: "agent-001", name: "Test Agent", permissionPolicy: undefined }, "FN-001", "run-1");
await ctx.pauseForApproval({
approvalRequestId: "apr-1",
decision: {
disposition: "require-approval",
category: "command_execution",
toolName: "bash",
operation: "git commit",
summary: "bash: git commit",
resourceType: "git",
approvalDedupeKey: "dedupe-1",
metadata: {},
},
});
expect(pauseTask).toHaveBeenCalledWith("FN-001", true, undefined, { pausedByAgentId: "agent-001" });
expect((store.updateAgentState as any)).toHaveBeenCalledWith("agent-001", "paused");
expect((store.updateAgent as any)).toHaveBeenCalledWith("agent-001", { pauseReason: "awaiting-approval" });
});
it("pauseForApproval still pauses agent when taskId is undefined", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const pauseTask = vi.fn().mockResolvedValue(undefined);
mockTaskStore = createMockTaskStore({ pauseTask });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const ctx = (monitor as any).buildActionGateContext({ id: "agent-001", name: "Test Agent", permissionPolicy: undefined }, undefined, "run-1");
await ctx.pauseForApproval({
approvalRequestId: "apr-1",
decision: {
disposition: "require-approval",
category: "command_execution",
toolName: "bash",
operation: "git commit",
summary: "bash: git commit",
resourceType: "git",
approvalDedupeKey: "dedupe-1",
metadata: {},
},
});
expect(pauseTask).not.toHaveBeenCalled();
expect((store.updateAgentState as any)).toHaveBeenCalledWith("agent-001", "paused");
expect((store.updateAgent as any)).toHaveBeenCalledWith("agent-001", { pauseReason: "awaiting-approval" });
});
it("omits permanent-agent gating context for ephemeral heartbeat agents", async () => {
const store = createStoreWithAgentForExec({
taskId: "FN-001",
metadata: { agentKind: "task-worker" },
name: "executor-ephemeral",
reportsTo: undefined,
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const args = mockedCreateFnAgent.mock.calls[0]?.[0] as {
permanentAgentGating?: unknown;
actionGateContext?: unknown;
};
expect(args.permanentAgentGating).toBeUndefined();
expect(args.actionGateContext).toBeUndefined();
});
describe("dependency validation", () => {
it("throws when taskStore is not configured", async () => {
const store = createStoreWithAgentForExec();
const monitor = new HeartbeatMonitor({ store, rootDir: "/tmp" });
await expect(
monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" })
).rejects.toThrow("HeartbeatMonitor not configured for execution (missing taskStore/rootDir)");
});
it("throws when rootDir is not configured", async () => {
const store = createStoreWithAgentForExec();
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore });
await expect(
monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" })
).rejects.toThrow("HeartbeatMonitor not configured for execution (missing taskStore/rootDir)");
});
});
describe("graceful exit", () => {
it("completes with no_assignment when agent has no taskId and no explicit taskId", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "no_assignment" });
// Should NOT have created an agent session
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("completes with invalid_state when agent state is paused", async () => {
const store = createStoreWithAgentForExec({ state: "paused" });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "invalid_state", state: "paused" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "active");
});
it("completes with invalid_state when agent state is error", async () => {
const store = createStoreWithAgentForExec({ state: "error" });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "invalid_state", state: "error" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "active");
});
it("keeps terminated as a run status while pausing the agent", async () => {
const store = createStoreWithAgentForExec({ state: "running" });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const run = await monitor.startRun("agent-001", { source: "on_demand" });
await monitor.completeRun("agent-001", run.id, {
status: "terminated",
stderrExcerpt: "Run stopped by user",
});
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "running");
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "paused");
expect(store.endHeartbeatRun).toHaveBeenCalledWith(run.id, "terminated");
});
it("clears stale lastError after a subsequent successful heartbeat run", async () => {
const store = createStoreWithAgentForExec({ state: "running" });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const failedRun = await monitor.startRun("agent-001", { source: "on_demand" });
await monitor.completeRun("agent-001", failedRun.id, {
status: "failed",
stderrExcerpt: "Prompt failed",
});
const successfulRun = await monitor.startRun("agent-001", { source: "on_demand" });
await monitor.completeRun("agent-001", successfulRun.id, {
status: "completed",
});
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "error");
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", { lastError: "Prompt failed" });
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", { lastError: undefined });
});
it("completes as failed when agent not found in store", async () => {
const store = createStoreWithAgentForExec();
(store.getAgent as ReturnType<typeof vi.fn>).mockResolvedValue(null);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("failed");
expect(result.stderrExcerpt).toContain("not found");
});
it("completes with task_not_found when task does not exist", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-MISSING" });
mockTaskStore.getTask = vi.fn().mockRejectedValue(new Error("Task FN-MISSING not found"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "task_not_found", taskId: "FN-MISSING" });
});
it("clears archived task assignments and falls back to a no-task heartbeat for identity agents", async () => {
const appendAgentLog = vi.fn().mockResolvedValue(undefined);
mockTaskStore = createMockTaskStore({
appendAgentLog,
getTask: vi.fn().mockResolvedValue({
id: "FN-ARCHIVED",
title: "Archived Task",
description: "Archived task description",
prompt: "# Archived\n\nTask is archived",
steps: [],
column: "archived",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const store = createStoreWithAgentForExec({
taskId: "FN-ARCHIVED",
soul: "Monitor the project and handle ambient work.",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect((store.assignTask as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"agent-001",
undefined,
expect.objectContaining({ agentId: "agent-001", source: "on_demand" }),
);
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const toolNames = mockedCreateFnAgent.mock.calls[0]![0]!.customTools!.map((tool: any) => tool.name);
expect(toolNames).not.toContain("fn_task_log");
expect(toolNames).not.toContain("fn_task_document_write");
expect(toolNames).not.toContain("fn_task_document_read");
expect(appendAgentLog).not.toHaveBeenCalled();
});
it("exits gracefully for explicit terminal task overrides that are not the agent's current assignment", async () => {
mockTaskStore = createMockTaskStore({
getTask: vi.fn().mockResolvedValue({
id: "FN-DONE",
title: "Done Task",
description: "Done task description",
prompt: "# Done\n\nTask is done",
steps: [],
column: "done",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const store = createStoreWithAgentForExec({
taskId: "FN-LIVE",
soul: "Stay helpful.",
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
taskId: "FN-DONE",
});
expect(result.status).toBe("completed");
expect(result.resultJson).toEqual({ reason: "terminal_task", taskId: "FN-DONE", column: "done" });
expect(store.assignTask).not.toHaveBeenCalledWith("agent-001", undefined, expect.anything());
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
});
// ── Identity Agents Without Tasks ─────────────────────────────────────────────
// FN-2051: Agents with identity (soul, instructions, memory) should run heartbeat
// sessions even without a task assignment, enabling them to do ambient work like
// messaging, memory management, task creation, and delegation.
describe("identity agents without tasks", () => {
it("agent WITH soul but no task creates session and completes successfully", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator agent who monitors project health" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
// Should create a session
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
// Reason should indicate identity run
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
});
it("auto-claim disabled skips candidate claiming during no-task runs", async () => {
const store = createStoreWithAgentForExec({
taskId: undefined,
soul: "I am a coordinator",
runtimeConfig: { autoClaimRelevantTasks: false },
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
mockTaskStore = createMockTaskStore({
listTasks: vi.fn().mockResolvedValue([
{
id: "FN-CANDIDATE",
description: "executor workflow cleanup",
title: "Executor cleanup",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail,
]),
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect((store.claimTaskForAgent as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
expect(executionPrompt).toContain("auto-claim relevant tasks: disabled");
});
it("auto-claim enabled attempts to claim relevant no-task candidates", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "executor reliability owner" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
mockTaskStore = createMockTaskStore({
listTasks: vi.fn().mockResolvedValue([
{
id: "FN-CANDIDATE",
description: "executor reliability follow-up",
title: "Executor reliability",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail,
]),
getTask: vi.fn().mockImplementation(async (id: string) => ({
id,
title: "Executor reliability",
description: "executor reliability follow-up",
prompt: "# PROMPT",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail)),
});
(store.claimTaskForAgent as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
task: { id: "FN-CANDIDATE" },
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(store.claimTaskForAgent).toHaveBeenCalledWith(
"agent-001",
"FN-CANDIDATE",
expect.objectContaining({ agentId: "agent-001", source: "timer" }),
);
const toolNames = mockedCreateFnAgent.mock.calls[0]![0]!.customTools!.map((tool: any) => tool.name);
expect(toolNames).toContain("fn_task_log");
});
it("auto-claim skips implementation candidates for non-executor agents", async () => {
const store = createStoreWithAgentForExec({
taskId: undefined,
role: "reviewer",
soul: "review workflows",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
mockTaskStore = createMockTaskStore({
listTasks: vi.fn().mockResolvedValue([
{
id: "FN-CANDIDATE",
description: "executor reliability follow-up",
title: "Executor reliability",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail,
]),
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(store.claimTaskForAgent).not.toHaveBeenCalled();
});
it("agent WITH instructionsText but no task creates session and completes successfully", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, instructionsText: "Monitor task board and create follow-up tasks" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
});
it("agent WITH memory but no task creates session and completes successfully", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, memory: "Last week we shipped the new API. Watch for integration issues." });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "no_assignment_identity_run" }));
});
it("ephemeral agent with soul but no task still bails with no_assignment", async () => {
// Ephemeral agents (agentKind: "task-worker") should NOT run no-task sessions
const store = createStoreWithAgentForExec({
taskId: undefined,
soul: "I am a task worker",
metadata: { agentKind: "task-worker" },
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
// Ephemeral agents should NOT create a session
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
// Should still exit with no_assignment (not no_assignment_identity_run)
expect(result.resultJson).toEqual({ reason: "no_assignment" });
});
it("identity agent without task receives correct tools (fn_task_create, fn_list_agents, fn_delegate_task, fn_heartbeat_done)", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
expect(callArgs.tools).toBe("coding");
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should have fn_task_create, fn_list_agents, fn_delegate_task
expect(toolNames).toContain("fn_task_create");
expect(toolNames).toContain("fn_list_agents");
expect(toolNames).toContain("fn_delegate_task");
// Should have fn_heartbeat_done
expect(toolNames).toContain("fn_heartbeat_done");
// Should have memory tools
expect(toolNames).toContain("fn_memory_search");
expect(toolNames).toContain("fn_memory_append");
// Should NOT have fn_task_log, fn_task_document_write, fn_task_document_read (they require taskId)
expect(toolNames).not.toContain("fn_task_log");
expect(toolNames).not.toContain("fn_task_document_write");
expect(toolNames).not.toContain("fn_task_document_read");
});
it("no-task run receives HEARTBEAT_NO_TASK_SYSTEM_PROMPT as system prompt", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
expect(systemPrompt).not.toContain("fn_task_log");
expect(systemPrompt).not.toContain("fn_task_document_write");
expect(systemPrompt).not.toContain("fn_task_document_read");
expect(systemPrompt).toContain("fn_task_create");
expect(systemPrompt).toContain("fn_list_agents");
expect(systemPrompt).toContain("fn_delegate_task");
expect(systemPrompt).toContain("fn_read_messages");
expect(systemPrompt).toContain("fn_send_message");
expect(systemPrompt).toContain("fn_memory_search");
expect(systemPrompt).toContain("fn_memory_append");
expect(systemPrompt).toContain('scope="agent"');
expect(systemPrompt).toContain('scope="project"');
expect(systemPrompt).toContain("fn_heartbeat_done");
});
it("identity agent without task receives no-task execution prompt mentioning 'no assigned task'", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_NO_TASK_SYSTEM_PROMPT);
expect(systemPrompt).not.toContain("fn_task_log");
expect(systemPrompt).not.toContain("fn_task_document_write");
expect(systemPrompt).not.toContain("fn_task_document_read");
expect(systemPrompt).not.toContain("Task Documents:");
expect(systemPrompt).toContain("fn_task_create");
expect(systemPrompt).toContain("fn_heartbeat_done");
expect(systemPrompt).toContain("fn_memory_append");
// The execution prompt is passed to session.prompt by promptWithFallback mock
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1]![0]!;
// Should mention no assigned task
expect(executionPrompt).toContain("No assigned task");
// Should describe ambient work capabilities
expect(executionPrompt).toContain("ambient work");
expect(executionPrompt).toContain("fn_task_create");
expect(executionPrompt).toContain("fn_list_agents");
expect(executionPrompt).toContain("fn_delegate_task");
// Should NOT include task-specific content
expect(executionPrompt).not.toContain("Assigned task:");
expect(executionPrompt).not.toContain("Task description:");
// Should include Wake Delta + no-task heartbeat procedure (tool-aligned per-tick anchoring)
expect(executionPrompt).toContain("## Wake Delta");
expect(executionPrompt).toContain("wake reason:");
expect(executionPrompt).toContain("autonomous heartbeat run");
expect(executionPrompt).toContain(HEARTBEAT_NO_TASK_PROCEDURE);
});
it("task-scoped run receives HEARTBEAT_SYSTEM_PROMPT as system prompt", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const systemPrompt = callArgs.systemPrompt;
expect(systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(systemPrompt).toContain("fn_task_log");
expect(systemPrompt).toContain("fn_task_document_write");
expect(systemPrompt).toContain("Task Documents:");
});
it("timer task-scoped execution prompt is framed as autonomous heartbeat work", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-001" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1]![0]!;
expect(executionPrompt).toContain("## Wake Delta");
expect(executionPrompt).toContain("wake reason: timer");
expect(executionPrompt).toContain("autonomous heartbeat run");
});
it("identity agent without task gets soul in system prompt", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a CEO who prioritizes high-impact work" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
// Soul should be in the system prompt
expect(callArgs.systemPrompt).toContain("## Soul");
expect(callArgs.systemPrompt).toContain("I am a CEO who prioritizes high-impact work");
});
it("builds heartbeat system prompt with inline + file instructions plus soul and memory", async () => {
const tmpRoot = mkdtempSync(join(tmpdir(), "fn-hb-instr-"));
try {
writeFileSync(join(tmpRoot, "instructions.md"), "File-backed operating instruction", "utf-8");
const store = createStoreWithAgentForExec({
taskId: undefined,
instructionsText: "Inline operating instruction",
instructionsPath: "instructions.md",
soul: "I am an autonomous agent",
memory: "Remember to prefer concrete actions",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: tmpRoot });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
expect(callArgs.systemPrompt).toContain("Inline operating instruction");
expect(callArgs.systemPrompt).toContain("File-backed operating instruction");
expect(callArgs.systemPrompt).toContain("## Soul");
expect(callArgs.systemPrompt).toContain("## Agent Memory");
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
});
it("agent WITHOUT identity (no soul, instructions, memory) still exits with no_assignment", async () => {
// Agent with empty strings should also exit gracefully
const store = createStoreWithAgentForExec({
taskId: undefined,
soul: "",
instructionsText: "",
memory: "",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
// Should NOT create a session for agents without identity
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
expect(result.resultJson).toEqual({ reason: "no_assignment" });
});
it("identity agent without task includes messaging tools when messageStore is available", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue([]),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should have messaging tools when messageStore is available
expect(toolNames).toContain("fn_send_message");
expect(toolNames).toContain("fn_read_messages");
});
it("identity agent without task does NOT include messaging tools when messageStore is unavailable", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
// Should NOT have messaging tools when messageStore is not available
expect(toolNames).not.toContain("fn_send_message");
expect(toolNames).not.toContain("fn_read_messages");
});
it("identity agent without task fetches messages and includes them in prompt for timer trigger", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, soul: "I am a coordinator" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-notask-1",
fromId: "user-1",
content: "Please check the task board",
}),
createMessage({
id: "msg-notask-2",
fromId: "agent-5",
content: "Delegating FN-100 to you",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "timer",
triggerDetail: "scheduled",
});
expect(result.status).toBe("completed");
// Messages should be fetched for no-task runs too
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
// Messages should be marked as read after successful execution
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
// Verify execution prompt included the messages
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("Please check the task board");
expect(executionPrompt).toContain("Delegating FN-100 to you");
});
});
describe("blocked-task heartbeat: runs through without early exit", () => {
it("invokes the model when task is blocked (no early exit)", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-BLOCKED" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const taskDetail = {
id: "FN-BLOCKED",
title: "Blocked Task",
description: "Blocked task description",
prompt: "",
status: "queued",
blockedBy: "FN-DEP-1",
comments: [],
steeringComments: [],
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail;
mockTaskStore = createMockTaskStore({ getTask: vi.fn().mockResolvedValue(taskDetail) });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
// The heartbeat must fall through to model invocation — no early exit reason
expect(result.resultJson).not.toEqual(expect.objectContaining({ reason: "blocked" }));
expect(result.resultJson).not.toEqual(expect.objectContaining({ reason: "blocked_duplicate" }));
expect(mockedCreateFnAgent).toHaveBeenCalled();
expect(mockSession.prompt).toHaveBeenCalled();
});
it("includes blockedBy in the prompt context when task is blocked", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-BLOCKED" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const taskDetail = {
id: "FN-BLOCKED",
title: "Blocked by dependency",
description: "Task blocked on FN-DEP-99",
prompt: "",
status: "queued",
blockedBy: "FN-DEP-99",
comments: [],
steeringComments: [],
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail;
mockTaskStore = createMockTaskStore({ getTask: vi.fn().mockResolvedValue(taskDetail) });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const promptCall = mockSession.prompt.mock.calls[0]?.[0] as string | undefined;
expect(promptCall).toBeDefined();
expect(promptCall).toContain("FN-DEP-99");
});
});
// ── Utility Lane Independence Regression ─────────────────────────────────────
// FN-1727: Heartbeat runs must execute on the control-plane (utility) lane
// and must NOT consume task-lane semaphore slots. This test proves that
// heartbeat execution completes successfully even when task execution
// slots are saturated (e.g., maxConcurrent: 0 or all slots occupied).
// The utility AI helper path must remain responsive under task-lane pressure.
describe("slot-saturation: heartbeat runs on utility lane independent of task-lane semaphore", () => {
it("executes heartbeat successfully while task-lane semaphore is saturated", async () => {
// Import AgentSemaphore directly to create a saturated slot fixture
const { AgentSemaphore } = await import("../concurrency.js");
// Create a semaphore with maxConcurrent=0 to simulate fully saturated state
// The defensive guard in AgentSemaphore.limit returns minimum 1, so we
// use a static limit of 0 and manually acquire to simulate saturation.
const taskLaneSemaphore = new AgentSemaphore(0);
// Acquire the single available slot to saturate task lanes
await taskLaneSemaphore.acquire();
// Verify the semaphore is saturated (no available slots)
expect(taskLaneSemaphore.availableCount).toBe(0);
expect(taskLaneSemaphore.activeCount).toBe(1);
// Create the heartbeat monitor (it does NOT receive the task-lane semaphore)
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
// Execute heartbeat while task lanes are saturated
// This MUST succeed because heartbeat runs on the utility lane
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
// CRITICAL ASSERTIONS:
// 1. Heartbeat completed successfully (proves it didn't wait for task-lane slot)
expect(result).toBeDefined();
expect(result.status).toBe("completed");
// 2. Agent session was created (proves execution proceeded)
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
// 3. Semaphore saturation is still held (proves heartbeat didn't consume task-lane slot)
expect(taskLaneSemaphore.activeCount).toBe(1);
// 4. Semaphore available count is still 0 (still saturated from task-lane perspective)
expect(taskLaneSemaphore.availableCount).toBe(0);
// Cleanup: release the task-lane slot
taskLaneSemaphore.release();
expect(taskLaneSemaphore.activeCount).toBe(0);
});
it("completes on_demand heartbeat while task-lane slots are fully occupied", async () => {
const { AgentSemaphore } = await import("../concurrency.js");
// Simulate multiple task-lane agents holding all slots
const taskLaneSemaphore = new AgentSemaphore(2);
// Saturate both slots with "task-lane agents"
await taskLaneSemaphore.acquire(); // Agent 1
await taskLaneSemaphore.acquire(); // Agent 2
expect(taskLaneSemaphore.availableCount).toBe(0);
// Now execute heartbeat - it should complete without waiting
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const startTime = Date.now();
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const elapsed = Date.now() - startTime;
// Should complete quickly (not blocked by semaphore wait)
expect(elapsed).toBeLessThan(500);
// Heartbeat should succeed
expect(result.status).toBe("completed");
// Task-lane slots should remain occupied
expect(taskLaneSemaphore.activeCount).toBe(2);
// Cleanup
taskLaneSemaphore.release();
taskLaneSemaphore.release();
});
});
describe("executeHeartbeat - message processing", () => {
it("includes unread messages in prompt when woken by wake-on-message", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-1",
fromId: "agent-2",
fromType: "agent",
content: "Hello from agent-2",
createdAt: "2024-01-15T10:30:00.000Z",
}),
createMessage({
id: "msg-2",
fromId: "user-1",
content: "Hello from user",
createdAt: "2024-01-15T11:00:00.000Z",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalled();
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
// Verify execution prompt (passed to promptWithFallback) included the messages
// The execution prompt is passed to session.prompt by promptWithFallback mock
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("[id: msg-1] [from: agent:agent-2] Hello from agent-2");
expect(executionPrompt).toContain("[id: msg-2] [from: user:user-1] Hello from user");
// Task-scoped prompts must include Wake Delta + Heartbeat Procedure so
// the agent re-runs its procedure each tick instead of grinding on the
// assigned task (paperclip-parity).
expect(executionPrompt).toContain("## Wake Delta");
expect(executionPrompt).toContain("wake reason: message_received");
expect(executionPrompt).toContain("autonomous heartbeat run");
expect(executionPrompt).toContain(HEARTBEAT_PROCEDURE);
expect(executionPrompt).toContain("do not re-read PROMPT.md to advance it");
});
it("substitutes per-agent heartbeatProcedurePath content for the default procedure", async () => {
const tmpRoot = mkdtempSync(join(tmpdir(), "fn-hb-procedure-"));
try {
const customProcedure = "## Custom CEO Procedure\n\n1. Review reports\n2. Update strategy\n3. Exit";
writeFileSync(join(tmpRoot, "MY-PROCEDURE.md"), customProcedure, "utf-8");
const store = createStoreWithAgentForExec({
heartbeatProcedurePath: "MY-PROCEDURE.md",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: tmpRoot });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
// Custom procedure should appear; default constant should not.
expect(executionPrompt).toContain("## Custom CEO Procedure");
expect(executionPrompt).toContain("1. Review reports");
expect(executionPrompt).not.toContain(HEARTBEAT_PROCEDURE);
// Wake Delta still rendered.
expect(executionPrompt).toContain("## Wake Delta");
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
});
it("falls back to default procedure when heartbeatProcedurePath is invalid (traversal)", async () => {
const store = createStoreWithAgentForExec({
heartbeatProcedurePath: "../escape.md",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
const promptCalls = mockSession.prompt.mock.calls;
const executionPrompt = promptCalls[promptCalls.length - 1][0];
// Invalid path → fall back to the default constant.
expect(executionPrompt).toContain(HEARTBEAT_PROCEDURE);
});
it("does not include message section when no unread messages", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue([]),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("completed");
// Verify prompt did NOT include pending messages section
// Note: without wake-on-message trigger, no messages are fetched
// so the prompt won't have the Pending Messages section at all
});
it("marks messages as read after successful heartbeat execution", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-1",
fromId: "agent-2",
content: "Hello from agent-2",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("completed");
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
});
it("does not mark messages as read on failed heartbeat execution", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Execution failed"));
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-1",
fromId: "agent-2",
content: "Hello from agent-2",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("failed");
expect(messageStore.markAllAsRead).not.toHaveBeenCalled();
});
it("fetches messages for timer-triggered runs when messageStore is available", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-1",
fromId: "agent-2",
content: "Reminder about task FN-001",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
// Use a timer trigger (not wake-on-message)
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "timer",
triggerDetail: "scheduled",
});
expect(result.status).toBe("completed");
// Messages should be fetched even for timer triggers
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
// Messages should be marked as read after successful execution
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
// Verify execution prompt included the messages
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("Reminder about task FN-001");
});
it("fetches messages for assignment-triggered runs when messageStore is available", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-assign-1",
fromId: "user-1",
content: "Please work on this task",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
// Use an assignment trigger
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "assignment",
triggerDetail: "task-assigned",
});
expect(result.status).toBe("completed");
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
// Verify execution prompt included the messages
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("Please work on this task");
});
it("fetches messages for on-demand runs without wake-on-message trigger", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-od-1",
fromId: "agent-3",
content: "Status update: task FN-002 is complete",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
// Use on-demand trigger without wake-on-message
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "manual",
});
expect(result.status).toBe("completed");
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
// Verify execution prompt included the messages
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("Status update: task FN-002 is complete");
});
it("still fetches messages for wake-on-message triggers", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const messages = [
createMessage({
id: "msg-wom-1",
fromId: "agent-2",
content: "Hello from agent-2",
}),
];
const messageStore = {
setMessageToAgentHook: vi.fn(),
getInbox: vi.fn().mockReturnValue(messages),
markAllAsRead: vi.fn(),
} as unknown as MessageStore;
const monitor = new HeartbeatMonitor({
store,
messageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("completed");
expect(messageStore.getInbox).toHaveBeenCalledWith("agent-001", "agent", { read: false, limit: 10 });
expect(messageStore.markAllAsRead).toHaveBeenCalledWith("agent-001", "agent");
// Verify execution prompt included the messages
const promptCalls = mockSession.prompt.mock.calls;
expect(promptCalls.length).toBeGreaterThan(0);
const executionPrompt = promptCalls[promptCalls.length - 1][0];
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain("Hello from agent-2");
});
describe("end-to-end message flow", () => {
it("proves wake-on-message can surface a user message and send a linked reply", async () => {
const messages: Map<string, Message[]> = new Map();
let messageCounter = 0;
const fakeMessageStore = {
setMessageToAgentHook: vi.fn(),
sendMessage: vi.fn((input: Omit<Message, "id" | "read" | "createdAt" | "updatedAt">) => {
const id = `msg-${++messageCounter}`;
const createdAt = new Date().toISOString();
const msg: Message = {
id,
...input,
read: false,
createdAt,
updatedAt: createdAt,
};
const key = `${input.toId}:${input.toType}`;
const inbox = messages.get(key) || [];
inbox.push(msg);
messages.set(key, inbox);
return msg;
}),
getInbox: vi.fn((participantId: string, participantType: string, opts?: { read?: boolean }) => {
const key = `${participantId}:${participantType}`;
const inbox = messages.get(key) || [];
if (opts?.read === false) return inbox.filter((message) => !message.read);
return inbox;
}),
getMailbox: vi.fn((participantId: string, participantType: string) => {
const key = `${participantId}:${participantType}`;
const inbox = messages.get(key) || [];
return { unreadCount: inbox.filter((message) => !message.read).length, messages: inbox };
}),
markAllAsRead: vi.fn((participantId: string, participantType: string) => {
const key = `${participantId}:${participantType}`;
const inbox = messages.get(key) || [];
inbox.forEach((message) => {
message.read = true;
});
messages.set(key, inbox);
}),
} as unknown as MessageStore;
const inboundFromUser = fakeMessageStore.sendMessage({
fromId: "dashboard",
fromType: "user",
toId: "agent-beta",
toType: "agent",
content: "Can you post a status update?",
type: "user-to-agent",
});
const store = createStoreWithAgentForExec({
id: "agent-beta",
name: "Agent Beta",
state: "active",
taskId: "FN-001",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({
store,
messageStore: fakeMessageStore,
taskStore: mockTaskStore,
rootDir: "/tmp",
});
const result = await monitor.executeHeartbeat({
agentId: "agent-beta",
source: "on_demand",
triggerDetail: "wake-on-message",
});
expect(result.status).toBe("completed");
const promptCalls = mockSession.prompt.mock.calls;
const executionPrompt = promptCalls[promptCalls.length - 1]?.[0] as string;
expect(executionPrompt).toContain("Pending Messages:");
expect(executionPrompt).toContain(`[id: ${inboundFromUser.id}]`);
expect(executionPrompt).toContain("dashboard");
const callArgs = mockedCreateFnAgent.mock.calls[0]![0]!;
const sendMessageTool = callArgs.customTools?.find((tool: { name: string }) => tool.name === "fn_send_message");
expect(sendMessageTool).toBeDefined();
for (const [index, alias] of ["dashboard", "user:dashboard", "User: user:dashboard"].entries()) {
await sendMessageTool!.execute(
`tool-call-${index}`,
{
to_id: alias,
content: `Status: I am on it. (${index})`,
type: "agent-to-user",
reply_to_message_id: inboundFromUser.id,
},
undefined,
undefined,
{} as any,
);
}
const dashboardInbox = fakeMessageStore.getInbox("dashboard", "user");
for (const index of [0, 1, 2]) {
const linkedReply = dashboardInbox.find((message) => message.content === `Status: I am on it. (${index})`);
expect(linkedReply?.metadata).toEqual({ replyTo: { messageId: inboundFromUser.id } });
}
monitor.stop();
});
});
});
describe("executeHeartbeat - inbox selection", () => {
const makeInboxSelection = (
taskId: string,
priority: "in_progress" | "todo" | "blocked" = "todo",
sourceMetadata?: Record<string, unknown>,
) => {
const now = new Date().toISOString();
return {
task: {
id: taskId,
description: `Inbox task ${taskId}`,
column: priority === "in_progress" ? "in-progress" : "todo",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: now,
updatedAt: now,
...(sourceMetadata ? { sourceMetadata } : {}),
},
priority,
reason: `selected:${priority}`,
} as any;
};
it("when agent has no taskId, inbox selects a todo task and assigns it", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(makeInboxSelection("FN-INBOX", "todo"));
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent,
getTask: vi.fn().mockResolvedValue({
id: "FN-INBOX",
title: "Inbox Task",
description: "Inbox-selected task",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001", { id: "agent-001", role: "executor" });
expect(store.assignTask).toHaveBeenCalledWith("agent-001", "FN-INBOX", expect.objectContaining({ agentId: "agent-001" }));
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-INBOX");
});
it("explicit taskId override takes precedence over inbox selection", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(makeInboxSelection("FN-INBOX", "todo"));
mockTaskStore = createMockTaskStore({ selectNextTaskForAgent });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
taskId: "FN-EXPLICIT",
});
expect(selectNextTaskForAgent).not.toHaveBeenCalled();
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-EXPLICIT");
});
it("agent's existing taskId takes precedence over inbox selection", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-EXISTING" });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(makeInboxSelection("FN-INBOX", "todo"));
mockTaskStore = createMockTaskStore({ selectNextTaskForAgent });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).not.toHaveBeenCalled();
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-EXISTING");
});
it("allows non-executor inbox selection when override metadata is present", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, role: "reviewer" });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(
makeInboxSelection("FN-INBOX", "todo", { executorRoleOverride: true }),
);
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent,
getTask: vi.fn().mockResolvedValue({
id: "FN-INBOX",
title: "Inbox Task",
description: "Inbox-selected task",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
sourceMetadata: { executorRoleOverride: true },
} as unknown as TaskDetail),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001", { id: "agent-001", role: "reviewer" });
expect(store.assignTask).toHaveBeenCalledWith("agent-001", "FN-INBOX", expect.objectContaining({ agentId: "agent-001" }));
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "inbox_selected", taskId: "FN-INBOX" }));
});
it("when inbox returns null, heartbeat completes with no_assignment", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(null);
mockTaskStore = createMockTaskStore({ selectNextTaskForAgent });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001", { id: "agent-001", role: "executor" });
expect(result.resultJson).toEqual({ reason: "no_assignment" });
});
it("records inbox selection metadata in resultJson", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent: vi.fn().mockResolvedValue(makeInboxSelection("FN-INBOX", "todo")),
getTask: vi.fn().mockResolvedValue({
id: "FN-INBOX",
title: "Inbox Task",
description: "Inbox-selected task",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result.resultJson).toEqual(expect.objectContaining({
reason: "inbox_selected",
priority: "todo",
taskId: "FN-INBOX",
}));
});
it("supports in-progress inbox selections before todo", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent: vi.fn().mockResolvedValue(makeInboxSelection("FN-RESUME", "in_progress")),
getTask: vi.fn().mockResolvedValue({
id: "FN-RESUME",
title: "Resume task",
description: "Resume in-progress work",
prompt: "",
steps: [],
column: "in-progress",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-RESUME");
expect(result.resultJson).toEqual(expect.objectContaining({
reason: "inbox_selected",
priority: "in_progress",
taskId: "FN-RESUME",
}));
});
it("gracefully skips inbox selection when checkoutTask throws", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(makeInboxSelection("FN-CHECKOUT", "todo"));
const checkoutTask = vi.fn().mockRejectedValue(new Error("Task is already checked out"));
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent,
checkoutTask: checkoutTask as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001", { id: "agent-001", role: "executor" });
expect(checkoutTask).toHaveBeenCalledWith("FN-CHECKOUT", "agent-001", expect.objectContaining({ agentId: "agent-001" }));
expect(result.resultJson).toEqual({ reason: "no_assignment" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
});
describe("execution", () => {
it("no-task system prompt does not reference fn_task_log or task_document tools", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_log");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_write");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("fn_task_document_read");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).not.toContain("task_document");
});
it("no-task system prompt references only available tools", () => {
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_task_create");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_list_agents");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_delegate_task");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_send_message");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_read_messages");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_search");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_get");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_memory_append");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("fn_heartbeat_done");
});
it("no-task heartbeat procedure aligns with ambient tools", () => {
expect(HEARTBEAT_NO_TASK_PROCEDURE).not.toContain("fn_task_log");
expect(HEARTBEAT_NO_TASK_PROCEDURE).not.toContain("fn_task_document_write");
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("fn_task_create");
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("fn_delegate_task");
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("fn_memory_append");
});
it("task-scoped system prompt still references fn_task_log and fn_task_document tools", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_log");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_document_write");
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("fn_task_document tools");
});
it("both prompts include memory boundaries section", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("## Memory Boundaries");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("## Memory Boundaries");
});
it("both prompts instruct replies to include reply_to_message_id", () => {
expect(HEARTBEAT_SYSTEM_PROMPT).toContain("reply_to_message_id");
expect(HEARTBEAT_NO_TASK_SYSTEM_PROMPT).toContain("reply_to_message_id");
});
it("both heartbeat procedures prioritize inbox processing before wake delta", () => {
expect(HEARTBEAT_PROCEDURE).toContain("process unread/pending messages before any other action");
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("process unread/pending messages before any other action");
expect(HEARTBEAT_PROCEDURE.indexOf("**Inbox**")).toBeLessThan(HEARTBEAT_PROCEDURE.indexOf("**Wake delta**"));
expect(HEARTBEAT_NO_TASK_PROCEDURE.indexOf("**Inbox**")).toBeLessThan(HEARTBEAT_NO_TASK_PROCEDURE.indexOf("**Wake delta**"));
});
it("heartbeat procedures include scope-discipline guidance", () => {
expect(HEARTBEAT_PROCEDURE).toContain("executor-class");
expect(HEARTBEAT_PROCEDURE).toContain("blocked");
expect(HEARTBEAT_PROCEDURE).toContain("coordination-class");
expect(HEARTBEAT_PROCEDURE).toContain("do not re-read PROMPT.md to advance it");
expect(HEARTBEAT_PROCEDURE).toContain("avoid re-planning it");
const inboxIndex = HEARTBEAT_PROCEDURE.indexOf("**Inbox**");
const wakeDeltaIndex = HEARTBEAT_PROCEDURE.indexOf("**Wake delta**");
const classifyIndex = HEARTBEAT_PROCEDURE.indexOf("**Classify the bound task**");
const selfCheckIndex = HEARTBEAT_PROCEDURE.indexOf("**Per-tick self-check**");
const exitIndex = HEARTBEAT_PROCEDURE.indexOf("fn_heartbeat_done");
expect(classifyIndex).toBeGreaterThan(inboxIndex);
expect(classifyIndex).toBeGreaterThan(wakeDeltaIndex);
expect(selfCheckIndex).toBeGreaterThan(classifyIndex);
expect(selfCheckIndex).toBeLessThan(exitIndex);
expect(HEARTBEAT_NO_TASK_PROCEDURE).toContain("coordination-class");
expect(HEARTBEAT_NO_TASK_PROCEDURE).not.toContain("executor-class");
expect(HEARTBEAT_NO_TASK_PROCEDURE).not.toContain("do not re-read PROMPT.md to advance it");
});
it("no-task system prompt processing messages section does not reference fn_task_log", () => {
const processingMessagesSection = HEARTBEAT_NO_TASK_SYSTEM_PROMPT.split("## Processing Messages")[1] ?? "";
expect(processingMessagesSection).not.toContain("fn_task_log");
});
it("creates session with enriched system prompt and expected tools", async () => {
const store = createStoreWithAgentForExec({
soul: "Act like a practical teammate who prioritizes clarity.",
memory: "Recent runs found flaky tests in integration suites.",
instructionsText: "Always log blockers with actionable next steps.",
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.cwd).toBe("/tmp/test");
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(callArgs.systemPrompt).toContain("## Soul");
expect(callArgs.systemPrompt).toContain("Act like a practical teammate who prioritizes clarity.");
expect(callArgs.systemPrompt).toContain("## Agent Memory");
expect(callArgs.systemPrompt).toContain("Recent runs found flaky tests in integration suites.");
expect(callArgs.systemPrompt).toContain("Always log blockers with actionable next steps.");
expect(callArgs.systemPrompt).toContain("## Project Memory");
expect(callArgs.systemPrompt).toContain("fn_memory_search");
expect(callArgs.systemPrompt).toContain("fn_task_log");
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
expect(callArgs.tools).toBe("coding");
// fn_get_agent_config, fn_update_agent_config, fn_agent_create, fn_agent_delete, fn_read_evaluations, fn_update_identity,
// fn_web_fetch, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
expect(callArgs.customTools).toHaveLength(17);
expect(callArgs.customTools![0]!.name).toBe("fn_task_create");
expect(callArgs.customTools![1]!.name).toBe("fn_task_log");
expect(callArgs.customTools![2]!.name).toBe("fn_task_document_write");
expect(callArgs.customTools![3]!.name).toBe("fn_task_document_read");
expect(callArgs.customTools![4]!.name).toBe("fn_list_agents");
expect(callArgs.customTools![5]!.name).toBe("fn_delegate_task");
expect(callArgs.customTools![6]!.name).toBe("fn_get_agent_config");
expect(callArgs.customTools![7]!.name).toBe("fn_update_agent_config");
expect(callArgs.customTools![8]!.name).toBe("fn_agent_create");
expect(callArgs.customTools![9]!.name).toBe("fn_agent_delete");
expect(callArgs.customTools![10]!.name).toBe("fn_read_evaluations");
expect(callArgs.customTools![11]!.name).toBe("fn_update_identity");
expect(callArgs.customTools![12]!.name).toBe("fn_web_fetch");
expect(callArgs.customTools![13]!.name).toBe("fn_memory_search");
expect(callArgs.customTools![14]!.name).toBe("fn_memory_get");
expect(callArgs.customTools![15]!.name).toBe("fn_memory_append");
// fn_heartbeat_done is last (terminal tool)
expect(callArgs.customTools![16]!.name).toBe("fn_heartbeat_done");
});
it("loads workspace memory into system prompt and identity snapshot when inline memory is empty", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "heartbeat-workspace-memory-"));
mkdirSync(join(rootDir, ".fusion", "agent-memory", "agent-001"), { recursive: true });
writeFileSync(
join(rootDir, ".fusion", "agent-memory", "agent-001", "MEMORY.md"),
"workspace memory for heartbeat",
"utf-8",
);
try {
const store = createStoreWithAgentForExec({
memory: "",
instructionsText: undefined,
instructionsPath: undefined,
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.systemPrompt).toContain("## Agent Memory");
expect(callArgs.systemPrompt).toContain("workspace memory for heartbeat");
const executionPrompt = mockSession.prompt.mock.calls.at(-1)?.[0] as string;
expect(executionPrompt).toMatch(/- memory: loaded \(\d+ chars, sha256:[a-f0-9]{8}, source: workspace\)/);
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
it("includes memory instructions even when agent has no custom instructions", async () => {
const store = createStoreWithAgentForExec({
soul: undefined,
memory: undefined,
instructionsText: undefined,
instructionsPath: undefined,
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(callArgs.systemPrompt).toContain("## Project Memory");
});
it("includes markdown instructions files plus soul in heartbeat system prompts", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "heartbeat-agent-instructions-"));
writeFileSync(
join(rootDir, "heartbeat-agent.md"),
"# Heartbeat Playbook\n\nCheck messages first, then create focused follow-up tasks.",
);
try {
const store = createStoreWithAgentForExec({
instructionsPath: "heartbeat-agent.md",
soul: "Operate like a calm, systems-minded operator.",
});
const taskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ memoryEnabled: false }),
} as Partial<TaskStore>);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.systemPrompt).toContain(HEARTBEAT_SYSTEM_PROMPT);
expect(callArgs.systemPrompt).toContain("## Soul");
expect(callArgs.systemPrompt).toContain("Operate like a calm, systems-minded operator.");
expect(callArgs.systemPrompt).toContain("# Heartbeat Playbook");
expect(callArgs.systemPrompt).toContain("create focused follow-up tasks");
} finally {
rmSync(rootDir, { recursive: true, force: true });
}
});
it("omits memory tools and instructions when project memory is disabled", async () => {
const store = createStoreWithAgentForExec();
const taskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ memoryEnabled: false }),
} as Partial<TaskStore>);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((tool: any) => tool.name);
expect(callArgs.systemPrompt).not.toContain("## Project Memory");
expect(toolNames).not.toContain("fn_memory_search");
expect(toolNames).not.toContain("fn_memory_get");
expect(toolNames).not.toContain("fn_memory_append");
});
it("wires session memory tools to read agent long-term, dreams, and daily layers", async () => {
const store = createStoreWithAgentForExec({
name: "CEO",
memory: "Prioritize roadmap sequencing and delegate implementation follow-ups.",
});
const taskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ memoryBackendType: "file" }),
} as Partial<TaskStore>);
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const memorySearch = callArgs.customTools!.find((tool: any) => tool.name === "fn_memory_search") as any;
const memoryGet = callArgs.customTools!.find((tool: any) => tool.name === "fn_memory_get") as any;
const memoryAppend = callArgs.customTools!.find((tool: any) => tool.name === "fn_memory_append") as any;
expect(memorySearch).toBeDefined();
expect(memoryGet).toBeDefined();
expect(memoryAppend).toBeDefined();
await memoryAppend.execute("call-append-dream", {
scope: "agent",
layer: "daily",
content: "- Daily delegation note from heartbeat test",
}, undefined, undefined, undefined);
appendFileSync(
"/tmp/test/.fusion/agent-memory/agent-001/DREAMS.md",
"\n- Dream delegation theme from heartbeat test\n",
"utf-8",
);
const dreamsResult = await memorySearch.execute("call-search-1", {
query: "dream delegation theme",
limit: 5,
}, undefined, undefined, undefined);
const dailyResult = await memorySearch.execute("call-search-2", {
query: "daily delegation note",
limit: 5,
}, undefined, undefined, undefined);
expect(dreamsResult.content[0].text).toContain(".fusion/agent-memory/agent-001/DREAMS.md");
expect(dailyResult.content[0].text).toContain(".fusion/agent-memory/agent-001/");
const dreamsRead = await memoryGet.execute("call-get-1", {
path: ".fusion/agent-memory/agent-001/DREAMS.md",
startLine: 1,
lineCount: 40,
}, undefined, undefined, undefined);
expect(dreamsRead.content[0].text).toContain("Dream delegation theme from heartbeat test");
});
it("includes document tools in heartbeat session", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((t: any) => t.name);
expect(toolNames).toContain("fn_task_document_write");
expect(toolNames).toContain("fn_task_document_read");
});
it("fn_heartbeat_done is the terminal tool (last in array)", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp/test" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const toolNames = callArgs.customTools!.map((t: any) => t.name);
// fn_heartbeat_done should be last for stable terminal signaling
expect(toolNames[toolNames.length - 1]).toBe("fn_heartbeat_done");
});
it("calls promptWithFallback with task context", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment", triggerDetail: "new task assigned" });
expect(mockSession.prompt).toHaveBeenCalledOnce();
const promptArg = mockSession.prompt.mock.calls[0]![0] as string;
expect(promptArg).toContain("agent-001");
expect(promptArg).toContain("Test Task");
expect(promptArg).toContain("assignment");
expect(promptArg).toContain("new task assigned");
expect(promptArg).toContain("PROMPT.md");
});
it("includes triggering comment context in execution prompt when comment IDs are provided", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
mockTaskStore.getTask = vi.fn().mockResolvedValue({
id: "FN-001",
title: "Test Task",
description: "Test task description",
prompt: "# Prompt",
comments: [{ id: "c-1", author: "user", text: "Please cover edge cases", createdAt: "2026-01-01T00:00:00.000Z" }],
steeringComments: [{ id: "s-1", author: "agent", text: "Investigating blocker", createdAt: "2026-01-01T00:01:00.000Z" }],
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
triggeringCommentIds: ["c-1", "s-1"],
triggeringCommentType: "steering",
});
const promptArg = mockSession.prompt.mock.calls[0]![0] as string;
expect(promptArg).toContain("You were woken because of new comments on this task");
expect(promptArg).toContain("Please cover edge cases");
expect(promptArg).toContain("Investigating blocker");
});
it("keeps standard prompt when no triggering comments are provided", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const promptArg = mockSession.prompt.mock.calls[0]![0] as string;
expect(promptArg).not.toContain("You were woken because of new comments on this task");
expect(promptArg).not.toContain("New comments since last run:");
});
it("completes run with status completed on successful execution", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("completed");
// Agent state should be set back to active
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
// Session should be disposed
expect(mockSession.dispose).toHaveBeenCalled();
});
it("uses explicit taskId override instead of agent.taskId", async () => {
const store = createStoreWithAgentForExec({ taskId: "FN-DEFAULT" });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
// Override getTask to return a different task
mockTaskStore.getTask = vi.fn().mockResolvedValue({
id: "FN-OVERRIDE",
title: "Override Task",
description: "Override description",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as unknown as TaskDetail);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({
agentId: "agent-001",
source: "on_demand",
taskId: "FN-OVERRIDE",
});
// Should have fetched the override task
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-OVERRIDE");
// fn_task_log tool should use the override task ID
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
const taskLogTool = callArgs.customTools![1]!;
expect(taskLogTool.name).toBe("fn_task_log");
});
it("passes runtime model as primary and execution settings model as fallback", async () => {
const store = createStoreWithAgentForExec({
runtimeConfig: { model: "anthropic/claude-sonnet-4-5" },
});
mockTaskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({
executionProvider: "openai",
executionModelId: "gpt-4.1",
}),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.defaultProvider).toBe("anthropic");
expect(callArgs.defaultModelId).toBe("claude-sonnet-4-5");
expect(callArgs.fallbackProvider).toBe("openai");
expect(callArgs.fallbackModelId).toBe("gpt-4.1");
});
it("passes undefined model when runtimeConfig has no model", async () => {
const store = createStoreWithAgentForExec({ runtimeConfig: {} });
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const callArgs = mockedCreateFnAgent.mock.calls[0]![0];
expect(callArgs.defaultProvider).toBeUndefined();
expect(callArgs.defaultModelId).toBeUndefined();
});
it("persists contextSnapshot on run records", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "assignment",
triggerDetail: "task-assigned",
triggeringCommentIds: ["comment-1"],
triggeringCommentType: "task",
contextSnapshot: {
wakeReason: "assignment",
triggerDetail: "task-assigned",
taskId: "FN-001",
},
});
expect(result.contextSnapshot).toEqual({
wakeReason: "assignment",
triggerDetail: "task-assigned",
taskId: "FN-001",
triggeringCommentIds: ["comment-1"],
triggeringCommentType: "task",
});
});
it("persists automation source recovery context on run records", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({
agentId: "agent-001",
source: "automation",
triggerDetail: "self-healing durable-agent transient recovery",
contextSnapshot: {
selfHealing: {
reason: "transient-error",
attempt: 1,
source: "durable-agent-transient-error-recovery",
},
},
});
expect(result.contextSnapshot).toEqual(
expect.objectContaining({
selfHealing: {
reason: "transient-error",
attempt: 1,
source: "durable-agent-transient-error-recovery",
},
}),
);
});
it("records agent logs, context taskId, and stdoutExcerpt for successful runs", async () => {
const store = createStoreWithAgentForExec();
const appendAgentLog = vi.fn().mockResolvedValue(undefined);
mockTaskStore = createMockTaskStore({ appendAgentLog });
const mockSession = createMockAgentSession();
let onText: ((delta: string) => void) | undefined;
let onToolStart: ((name: string, args?: Record<string, unknown>) => void) | undefined;
let onToolEnd: ((name: string, isError: boolean, result?: unknown) => void) | undefined;
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
onText = opts.onText;
onToolStart = opts.onToolStart;
onToolEnd = opts.onToolEnd;
return { session: mockSession as any };
});
mockSession.prompt = vi.fn().mockImplementation(async () => {
onText?.("Heartbeat produced visible output");
onToolStart?.("read", { path: "README.md" });
onToolEnd?.("read", false, "done");
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "Heartbeat produced visible output", "text", undefined, "executor");
expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool", "README.md", "executor");
expect(appendAgentLog).toHaveBeenCalledWith("FN-001", "read", "tool_result", "done", "executor");
expect(result.contextSnapshot?.taskId).toBe("FN-001");
expect(result.stdoutExcerpt).toContain("Heartbeat produced visible output");
});
});
describe("fn_heartbeat_done tool", () => {
it("captures summary from fn_heartbeat_done in resultJson", async () => {
const store = createStoreWithAgentForExec();
let capturedDoneTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
// fn_heartbeat_done is last in the customTools array (index 4)
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
return { session: mockSession as any };
});
// Simulate: when prompt is called, invoke the fn_heartbeat_done tool
mockSession.prompt = vi.fn().mockImplementation(async (prompt: string) => {
// Simulate the agent calling fn_heartbeat_done
const result = await capturedDoneTool.execute("call-1", { summary: "Checked task, all good" });
expect(result.content[0].text).toContain("Heartbeat complete");
expect(result.content[0].text).toContain("Checked task, all good");
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const run = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(run.resultJson).toBeDefined();
expect((run.resultJson as any).summary).toBe("Checked task, all good");
});
it("works without summary in fn_heartbeat_done", async () => {
const store = createStoreWithAgentForExec();
let capturedDoneTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
capturedDoneTool = opts.customTools[opts.customTools.length - 1];
return { session: mockSession as any };
});
mockSession.prompt = vi.fn().mockImplementation(async () => {
await capturedDoneTool.execute("call-1", {});
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const run = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(run.resultJson).toBeDefined();
expect((run.resultJson as any).summary).toBeUndefined();
});
});
describe("fn_task_create tool", () => {
it("creates a task in the store when fn_task_create tool is called", async () => {
const store = createStoreWithAgentForExec();
let capturedCreateTool: any;
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
capturedCreateTool = opts.customTools[0]; // fn_task_create
return { session: mockSession as any };
});
mockSession.prompt = vi.fn().mockImplementation(async () => {
await capturedCreateTool.execute("call-1", { description: "Follow-up task" });
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(mockTaskStore.createTask).toHaveBeenCalledWith({
description: "Follow-up task",
dependencies: undefined,
column: "triage",
source: {
sourceType: "agent_heartbeat",
sourceAgentId: "agent-001",
sourceRunId: undefined,
},
}, expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
});
describe("error handling", () => {
it("completes run as failed when createFnAgent throws", async () => {
const store = createStoreWithAgentForExec();
mockedCreateFnAgent.mockRejectedValue(new Error("Model unavailable"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("failed");
expect(result.stderrExcerpt).toContain("Model unavailable");
// Agent state should be set to error
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "error");
});
it("fails soft on timer heartbeat when model provider credentials are unavailable", async () => {
const store = createStoreWithAgentForExec();
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(result.resultJson).toMatchObject({
reason: "heartbeat_model_unavailable",
source: "timer",
detail: expect.stringContaining("No API key for provider: anthropic"),
});
expect(result.stderrExcerpt).toContain("No API key for provider: anthropic");
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
});
it.each(["on_demand", "assignment"] as const)("pauses on %s heartbeat when model provider credentials are unavailable", async (source) => {
const store = createStoreWithAgentForExec();
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source });
expect(result.status).toBe("completed");
expect(result.resultJson).toMatchObject({
reason: "heartbeat_model_unavailable",
source,
actionRequired: true,
detail: expect.stringContaining("Configure credentials for provider \"anthropic\""),
});
expect(result.stderrExcerpt).toContain("No API key for provider: anthropic");
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "paused");
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", {
pauseReason: "heartbeat-model-unavailable",
lastError: expect.stringContaining("No API key for provider: anthropic"),
});
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
});
it("keeps timer-triggered credential failures in recoverable state across consecutive wakeups", async () => {
const store = createStoreWithAgentForExec();
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const first = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const second = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
for (const run of [first, second]) {
expect(run.status).toBe("completed");
expect(run.resultJson).toMatchObject({
reason: "heartbeat_model_unavailable",
source: "timer",
detail: expect.stringContaining("No API key for provider: anthropic"),
});
expect(run.stderrExcerpt).toContain("No API key for provider: anthropic");
}
expect(store.updateAgentState).toHaveBeenCalledWith("agent-001", "active");
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
});
it("keeps non-timer credential failures recoverable on consecutive wakeups", async () => {
const store = createStoreWithAgentForExec();
mockedCreateFnAgent.mockRejectedValue(new Error("No API key for provider: anthropic"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const first = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
const second = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
expect(first.status).toBe("completed");
expect(first.resultJson).toMatchObject({
reason: "heartbeat_model_unavailable",
source: "assignment",
actionRequired: true,
});
expect(second.status).toBe("completed");
expect(second.resultJson).toMatchObject({
reason: "heartbeat_model_unavailable",
source: "assignment",
actionRequired: true,
});
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "error");
});
it("completes run as failed when promptWithFallback throws", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Prompt failed"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result).toBeDefined();
expect(result.status).toBe("failed");
expect(result.stderrExcerpt).toContain("Prompt failed");
// Session should still be disposed in finally block
expect(mockSession.dispose).toHaveBeenCalled();
// Agent should be untracked
expect(monitor.getTrackedAgents()).not.toContain("agent-001");
});
it("flushes AgentLogger on execution failure", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
const flushSpy = vi.spyOn(AgentLogger.prototype, "flush").mockResolvedValue(undefined);
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Prompt failed"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(flushSpy).toHaveBeenCalled();
});
it("flushes AgentLogger when session creation fails", async () => {
const store = createStoreWithAgentForExec();
const flushSpy = vi.spyOn(AgentLogger.prototype, "flush").mockResolvedValue(undefined);
mockedCreateFnAgent.mockRejectedValue(new Error("Model unavailable"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(flushSpy).toHaveBeenCalled();
});
});
describe("concurrency", () => {
it("serializes concurrent executeHeartbeat calls for the same agent", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
let promptCallCount = 0;
// Make prompt take some time to ensure overlap
mockSession.prompt = vi.fn().mockImplementation(async () => {
promptCallCount++;
await new Promise((resolve) => setTimeout(resolve, 10));
});
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
// We need getRunDetail to return different runs for each call
let runCount = 0;
const concurrentSavedRuns: Map<string, AgentHeartbeatRun> = new Map();
(store.startHeartbeatRun as ReturnType<typeof vi.fn>).mockImplementation(async () => {
runCount++;
return {
id: `run-${runCount}`,
agentId: "agent-001",
startedAt: new Date().toISOString(),
endedAt: null,
status: "active",
} as AgentHeartbeatRun;
});
(store.saveRun as ReturnType<typeof vi.fn>).mockImplementation(async (run: AgentHeartbeatRun) => {
concurrentSavedRuns.set(run.id, run);
});
(store.getRunDetail as ReturnType<typeof vi.fn>).mockImplementation(async (_agentId: string, runId: string) => {
return concurrentSavedRuns.get(runId) ?? {
id: runId,
agentId: "agent-001",
startedAt: new Date().toISOString(),
endedAt: new Date().toISOString(),
status: "completed" as const,
};
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
// Fire two concurrent executions
const [result1, result2] = await Promise.all([
monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" }),
monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" }),
]);
// Both should complete
expect(result1).toBeDefined();
expect(result2).toBeDefined();
// Both should have called prompt (serialized, not concurrent)
expect(promptCallCount).toBe(2);
});
});
describe("usage tracking", () => {
it("records estimated output tokens in usageJson", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
let onTextCallback: ((delta: string) => void) | undefined;
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
onTextCallback = opts.onText;
return { session: mockSession as any };
});
// Simulate text output
mockSession.prompt = vi.fn().mockImplementation(async () => {
// Simulate 100 chars of output (roughly 25 tokens at 4 chars/token)
if (onTextCallback) {
onTextCallback("A".repeat(100));
}
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result.usageJson).toBeDefined();
expect(result.usageJson!.inputTokens).toBe(0);
expect(result.usageJson!.outputTokens).toBe(25); // 100/4 = 25
expect(result.usageJson!.cachedTokens).toBe(0);
});
it("accumulates usage on agent record", async () => {
const store = createStoreWithAgentForExec({
totalInputTokens: 100,
totalOutputTokens: 200,
});
const mockSession = createMockAgentSession();
let onTextCallback: ((delta: string) => void) | undefined;
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
onTextCallback = opts.onText;
return { session: mockSession as any };
});
mockSession.prompt = vi.fn().mockImplementation(async () => {
if (onTextCallback) {
onTextCallback("A".repeat(100));
}
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
// Should update cumulative tokens: 200 + 25 = 225
expect(store.updateAgent).toHaveBeenCalledWith("agent-001", {
totalInputTokens: 100,
totalOutputTokens: 225,
});
});
});
describe("cleanup", () => {
it("disposes session and untracks agent even on error", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
mockSession.prompt = vi.fn().mockRejectedValue(new Error("Crash"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
// Session disposed
expect(mockSession.dispose).toHaveBeenCalled();
// Agent untracked
expect(monitor.getTrackedAgents()).not.toContain("agent-001");
});
it("disposes session and untracks agent on success", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({
session: mockSession as any,
});
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(mockSession.dispose).toHaveBeenCalled();
expect(monitor.getTrackedAgents()).not.toContain("agent-001");
});
});
describe("Budget Governance", () => {
it("skips heartbeat when agent is over budget (timer)", async () => {
const budgetStatus = createBudgetStatus({
currentUsage: 10000,
budgetLimit: 10000,
usagePercent: 100,
thresholdPercent: 80,
isOverBudget: true,
isOverThreshold: true,
});
const store = createStoreWithAgentForExec();
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(budgetStatus);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted", budgetStatus });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
expect(store.updateAgentState).not.toHaveBeenCalledWith("agent-001", "active");
});
it("skips heartbeat when agent is over budget (on_demand)", async () => {
const store = createStoreWithAgentForExec();
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
createBudgetStatus({ isOverBudget: true, isOverThreshold: true, usagePercent: 100 })
);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("skips heartbeat when agent is over budget (assignment)", async () => {
const store = createStoreWithAgentForExec();
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
createBudgetStatus({ isOverBudget: true, isOverThreshold: true, usagePercent: 100 })
);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
expect(result.resultJson).toMatchObject({ reason: "budget_exhausted" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("skips timer heartbeat when agent is over threshold but not over budget", async () => {
const budgetStatus = createBudgetStatus({
currentUsage: 850,
budgetLimit: 1000,
usagePercent: 85,
thresholdPercent: 80,
isOverBudget: false,
isOverThreshold: true,
});
const store = createStoreWithAgentForExec();
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(budgetStatus);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.resultJson).toMatchObject({ reason: "budget_threshold_exceeded", budgetStatus });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("allows on_demand heartbeat when agent is over threshold", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
createBudgetStatus({ isOverThreshold: true, usagePercent: 85, budgetLimit: 1000, thresholdPercent: 80 })
);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
});
it("allows assignment heartbeat when agent is over threshold", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
createBudgetStatus({ isOverThreshold: true, usagePercent: 85, budgetLimit: 1000, thresholdPercent: 80 })
);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
});
it("proceeds normally when agent is below threshold", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockResolvedValue(
createBudgetStatus({ isOverBudget: false, isOverThreshold: false, usagePercent: 30, budgetLimit: 1000, thresholdPercent: 80 })
);
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
});
it("proceeds normally when getBudgetStatus throws", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
(store.getBudgetStatus as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("budget unavailable"));
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
});
});
describe("Pause Governance", () => {
it("skips heartbeat on global pause for timer source", async () => {
const store = createStoreWithAgentForExec();
const pauseAwareTaskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ globalPause: true, enginePaused: false }),
});
const monitor = new HeartbeatMonitor({ store, taskStore: pauseAwareTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(result.status).toBe("completed");
expect(result.resultJson).toMatchObject({ reason: "global_pause", source: "timer" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("skips heartbeat on global pause for assignment source", async () => {
const store = createStoreWithAgentForExec();
const pauseAwareTaskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ globalPause: true, enginePaused: false }),
});
const monitor = new HeartbeatMonitor({ store, taskStore: pauseAwareTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
expect(result.status).toBe("completed");
expect(result.resultJson).toMatchObject({ reason: "global_pause", source: "assignment" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
});
it("skips timer heartbeat on engine pause but allows assignment", async () => {
const timerStore = createStoreWithAgentForExec();
const pauseAwareTaskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: true }),
});
const timerMonitor = new HeartbeatMonitor({ store: timerStore, taskStore: pauseAwareTaskStore, rootDir: "/tmp" });
const timerResult = await timerMonitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
expect(timerResult.status).toBe("completed");
expect(timerResult.resultJson).toMatchObject({ reason: "engine_paused", source: "timer" });
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
const assignmentStore = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const assignmentMonitor = new HeartbeatMonitor({
store: assignmentStore,
taskStore: pauseAwareTaskStore,
rootDir: "/tmp",
});
const assignmentResult = await assignmentMonitor.executeHeartbeat({
agentId: "agent-001",
source: "assignment",
});
expect(assignmentResult.status).toBe("completed");
expect((assignmentResult.resultJson as Record<string, unknown>)?.reason).not.toBe("engine_paused");
expect(mockedCreateFnAgent).toHaveBeenCalledOnce();
});
it("proceeds when pause flags are false", async () => {
const store = createStoreWithAgentForExec();
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const pauseAwareTaskStore = createMockTaskStore({
getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false }),
});
const monitor = new HeartbeatMonitor({ store, taskStore: pauseAwareTaskStore, rootDir: "/tmp" });
const timerResult = await monitor.executeHeartbeat({ agentId: "agent-001", source: "timer" });
const onDemandResult = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
const assignmentResult = await monitor.executeHeartbeat({ agentId: "agent-001", source: "assignment" });
expect(timerResult.status).toBe("completed");
expect(onDemandResult.status).toBe("completed");
expect(assignmentResult.status).toBe("completed");
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(3);
});
});
});
// ── Task Creation Tracking Tests ──────────────────────────────────────