test(heartbeat): reproduce invisible monitor dispatch deferrals

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-19 14:20:22 -05:00
1 parent 8193abb936
commit 3f895b8403
2 files changed
+59 -1

No files matched your search

@@ -311,6 +311,40 @@ describeEmbeddedPostgres("issue monitor scheduler", () => {
expect((await db.select().from(agentWakeupRequests)).filter(wake => wake.reason === "issue_monitor_due")).toHaveLength(1);
});
it("explains held monitor dispatch without replaying uncertain execution", async () => {
const { companyId, issueId, agentId, nextCheckAt } = await seedFixture();
const sourceRunId = randomUUID();
await db.insert(heartbeatRuns).values({ id: sourceRunId, companyId, agentId,
runtimeMode: "legacy", status: "interrupted", errorCode: "process_lost",
finishedAt: new Date(), contextSnapshot: { issueId } });
const [hold] = await db.insert(issueRecoveryActions).values({ companyId, sourceIssueId: issueId,
kind: "active_run_watchdog", status: "active", ownerType: "board",
cause: "legacy_execution_requires_reconciliation", fingerprint: sourceRunId,
evidence: { runId: sourceRunId }, nextAction: "Inspect the interrupted execution before continuing." }).returning();
const heartbeat = heartbeatService(db);
const tickAt = new Date("2026-04-11T12:31:00.000Z");
expect((await heartbeat.tickTimers(tickAt)).enqueued).toBe(0);
expect((await heartbeat.tickTimers(new Date(tickAt.getTime() + 30_000))).enqueued).toBe(0);
const [waiting] = await db.select().from(issues).where(eq(issues.id, issueId));
expect(waiting.monitorNextCheckAt).toEqual(nextCheckAt);
expect(waiting.monitorWakeRequestedAt).toBeNull();
expect(waiting.monitorAttemptCount).toBe(0);
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId))).toHaveLength(1);
const diagnostics = (await db.select().from(activityLog).where(eq(activityLog.entityId, issueId)))
.filter(row => row.action === "issue.monitor_deferred");
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0]?.details).toMatchObject({ nextCheckAt: nextCheckAt.toISOString(),
reason: "execution_reconciliation_required", blockerCause: "legacy_execution_requires_reconciliation", attemptCount: 0 });
expect((await db.select().from(issueRecoveryActions).where(eq(issueRecoveryActions.id, hold!.id)))[0]?.status).toBe("active");
await db.update(issueRecoveryActions).set({ status: "resolved", resolvedAt: new Date(),
evidence: { runId: sourceRunId, executionReconciliation: { action: "continue" } } }).where(eq(issueRecoveryActions.id, hold!.id));
expect((await heartbeat.tickTimers(new Date(tickAt.getTime() + 60_000))).enqueued).toBe(1);
expect((await heartbeat.tickTimers(new Date(tickAt.getTime() + 90_000))).enqueued).toBe(0);
const [triggered] = await db.select().from(issues).where(eq(issues.id, issueId));
expect(triggered.monitorAttemptCount).toBe(1);
expect(triggered.monitorNextCheckAt).toBeNull();
});
it("recovers a rejected native monitor call and delivers the corrected schedule once", async () => {
const { companyId, issueId, agentId } = await seedFixture();
const runId = randomUUID();
@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import type { Db } from "@paperclipai/db";
import {
agents, agentSessionGoalActions, agentTaskSessions, companies, createDb,
activityLog, agents, agentSessionGoalActions, agentTaskSessions, companies, createDb,
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, heartbeatRuns, issues,
} from "@paperclipai/db";
import { eq, sql } from "drizzle-orm";
@@ -201,6 +201,30 @@ describe.skipIf(!postgres.supported)("heartbeat scheduling database boundary", (
expect(await f.scheduler.tickTimers(now)).toEqual({ checked: 1, enqueued: 1, skipped: 0 });
});
it("records bounded dispatch failure diagnostics while retaining the schedule", async () => {
const f = await fixture();
f.deps.enqueueWakeup.mockRejectedValue(new Error("PRIVATE_PROVIDER_FAILURE"));
await f.scheduler.tickTimers(now);
await f.scheduler.tickTimers(new Date(now.getTime() + 30_000));
expect(await readIssue(f.issue.id)).toMatchObject({ monitorNextCheckAt: dueAt, monitorWakeRequestedAt: null, monitorAttemptCount: 0 });
const events = await db.select().from(activityLog).where(eq(activityLog.entityId, f.issue.id));
expect(events.filter(event => event.action === "issue.monitor_dispatch_failed")).toHaveLength(1);
expect(events[0]?.details).toMatchObject({ reason: "monitor_dispatch_failed", nextCheckAt: dueAt.toISOString(), attemptCount: 0 });
expect(JSON.stringify(events)).not.toContain("PRIVATE_PROVIDER_FAILURE");
});
it("does not attach old dispatch diagnostics to a replacement monitor", async () => {
const f = await fixture();
const replacement = new Date(now.getTime() + 60_000);
f.deps.enqueueWakeup.mockImplementationOnce(async () => {
await db.update(issues).set({ monitorNextCheckAt: replacement, monitorWakeRequestedAt: null }).where(eq(issues.id, f.issue.id));
return null;
});
await f.scheduler.tickTimers(now);
expect(await readIssue(f.issue.id)).toMatchObject({ monitorNextCheckAt: replacement });
expect(await db.select().from(activityLog).where(eq(activityLog.entityId, f.issue.id))).toHaveLength(0);
});
it("leaves monitors alone while a native run owns the issue", async () => {
const f = await fixture();
const [run] = await db.insert(heartbeatRuns).values({ companyId: f.company.id, agentId: f.agent.id, nativeIssueId: f.issue.id, runtimeMode: "native", status: "running", contextSnapshot: { issueId: f.issue.id } }).returning();