refactor(heartbeat): collapse skip decisions

This commit is contained in:
ZacharyZhang-NY committed 2026-04-24 03:13:57 -04:00
1 parent 1383805d66
commit 8d98151027
7 files changed
+247 -197

No files matched your search

@@ -238,6 +238,24 @@ describe("buildSystemPrompt leadership classification", () => {
expect(changedFullPrompt).toContain("1 blocked");
});
it("keeps heartbeat skip state out of the task prompt", () => {
const ctx = buildCtx() as PromptContext & {
heartbeatSkip: { reason: string; streak: number; since: string; lastTickAt: string };
};
ctx.heartbeatSkip = {
reason: "worker_not_ready",
streak: 7,
since: "2026-04-23T00:00:00.000Z",
lastTickAt: "2026-04-23T00:10:00.000Z",
};
const prompt = buildTaskPrompt(ctx);
expect(prompt).not.toContain("Heartbeat Skip Status");
expect(prompt).not.toContain("worker_not_ready");
expect(prompt).not.toContain("Consecutive skips");
});
it("keeps volatile task context after the cacheable system prefix", () => {
const ctx = buildCtx({ wakeReason: "timer" });
const systemPrompt = buildSystemPrompt(ctx);
+152
View File
@@ -0,0 +1,152 @@
import { join } from "node:path";
import { tmpdir } from "node:os";
import { mkdtemp, rm } from "node:fs/promises";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("electron", () => ({
powerMonitor: {
on: vi.fn(),
removeListener: vi.fn(),
},
safeStorage: {
isEncryptionAvailable: () => false,
getSelectedStorageBackend: () => "basic_text",
encryptString: (value: string) => Buffer.from(value, "utf8"),
decryptString: (value: Buffer) => value.toString("utf8"),
},
}));
import { AppDatabase } from "../../src/main/database";
import { createHeartbeatScheduler } from "../../src/main/orchestration/scheduler-heartbeat";
import type { OrchestrationDependencies } from "../../src/main/orchestration/types";
function createLogger() {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
}
function createCompany(db: AppDatabase): string {
return db.core.company.saveCompany({
name: "Heartbeat Scheduler Co",
description: "Scheduler gate contract test",
status: "active",
});
}
function createConnector(db: AppDatabase): void {
db.core.connectors.saveConnector({
id: "openai-codex",
model: "gpt-5.4",
envBindingText: "",
notes: "Test connector",
});
}
function createWorkspace(db: AppDatabase, companyId: string, localPath: string): string {
return db.core.workspaces.saveWorkspace({
companyId,
name: "Primary",
localPath,
repoUrl: "",
repoRef: "",
isPrimary: true,
});
}
function createAgent(db: AppDatabase, companyId: string, workspaceId: string): string {
const agentId = db.core.agents.saveAgent({
companyId,
name: "Scheduler Agent",
role: "Engineer",
title: "Runtime Engineer",
status: "active",
reportsTo: null,
connectorId: "openai-codex",
workspaceId,
model: "gpt-5.4",
capabilities: "Checks scheduler heartbeat gates",
budgetMonthlyUsd: 100,
});
db.core.agents.setHeartbeat(agentId, true, 1);
return agentId;
}
function createDeps(db: AppDatabase): OrchestrationDependencies {
return {
db,
logger: createLogger(),
publishDomainChanged: vi.fn(),
emitEvent: vi.fn(),
notify: vi.fn(),
createRunToken: () => "test-token",
getApiPort: () => 3210,
dispatchAutomation: vi.fn(),
hasWorkerReady: () => false,
enqueueWorkerRun: vi.fn(),
isQuitting: () => false,
listRuntimeSkillEntries: () => [],
budgetService: {
evaluateBudgetGate: vi.fn(() => ({ allowed: true, blockedBy: [] })),
} as never,
checkpointEngine: { shouldPauseHeartbeat: () => false },
};
}
describe("heartbeat scheduler", () => {
let tempDir: string;
let db: AppDatabase;
beforeEach(async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-23T00:00:00.000Z"));
tempDir = await mkdtemp(join(tmpdir(), "agentcompany-scheduler-heartbeat-"));
db = new AppDatabase(tempDir);
db.init();
});
afterEach(async () => {
db.close();
await rm(tempDir, { recursive: true, force: true });
vi.useRealTimers();
});
it("records worker readiness skips from the heartbeat run decision", () => {
const companyId = createCompany(db);
createConnector(db);
const workspaceId = createWorkspace(db, companyId, tempDir);
const agentId = createAgent(db, companyId, workspaceId);
const startAgentHeartbeatRun = vi.fn(() => ({
status: "skipped" as const,
reason: "system_unavailable" as const,
detail: "worker_not_ready",
}));
const scheduler = createHeartbeatScheduler(
createDeps(db),
() => false,
startAgentHeartbeatRun,
);
try {
scheduler.start();
vi.advanceTimersByTime(15_000);
} finally {
scheduler.stop();
}
expect(startAgentHeartbeatRun).toHaveBeenCalledWith(agentId, companyId, "timer");
const activity = db.projection.snapshot.listSnapshot({ companyId }).activity;
const skip = activity.find((entry) => entry.action === "heartbeat.skipped");
expect(skip?.entityId).toBe(agentId);
expect(JSON.parse(skip?.detail ?? "{}")).toMatchObject({
agentName: "Scheduler Agent",
reason: "system_unavailable",
detail: "worker_not_ready",
trigger: "timer",
});
expect(db.core.agents.getRuntimeState(agentId)).not.toHaveProperty("heartbeatSkip");
});
});