From 77bc236c2ac6fb2de6cbb03f2ea60f0093b04cf2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 1 Jul 2026 19:17:24 +0000 Subject: [PATCH] Add runtime heartbeat release gate tests --- .../heartbeat-process-recovery.test.ts | 74 +++++++++++++++++++ .../issue-stale-execution-lock-routes.test.ts | 61 +++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/server/src/__tests__/heartbeat-process-recovery.test.ts b/server/src/__tests__/heartbeat-process-recovery.test.ts index af0c89a8bdb..09d0063cb52 100644 --- a/server/src/__tests__/heartbeat-process-recovery.test.ts +++ b/server/src/__tests__/heartbeat-process-recovery.test.ts @@ -811,6 +811,40 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { return { companyId, agentId, issueId }; } + async function seedIdleTimerAgentFixture() { + const companyId = randomUUID(); + const agentId = randomUUID(); + const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`; + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix, + requireBoardApprovalForNewAgents: false, + }); + + await db.insert(agents).values({ + id: agentId, + companyId, + name: "CodexCoder", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: { + heartbeat: { + enabled: true, + intervalSec: 60, + wakeOnDemand: true, + skipTimerWhenNoActionableWork: true, + }, + }, + permissions: {}, + }); + + return { companyId, agentId }; + } + async function expectSourceScopedStrandedRecoveryAction(input: { companyId: string; agentId: string; @@ -1042,6 +1076,46 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => { expect(wakeup?.status).toBe("claimed"); }); + it("skips generic timer wakes without invoking an adapter when no assigned work is actionable", async () => { + const { companyId, agentId } = await seedIdleTimerAgentFixture(); + const heartbeat = heartbeatService(db); + + const run = await heartbeat.wakeup(agentId, { + source: "timer", + triggerDetail: "system", + reason: "heartbeat_timer", + requestedByActorType: "system", + requestedByActorId: "heartbeat_scheduler", + contextSnapshot: { + source: "scheduler", + reason: "interval_elapsed", + now: "2026-03-19T00:00:00.000Z", + }, + }); + + expect(run).toBeNull(); + expect(mockAdapterExecute).not.toHaveBeenCalled(); + + const requests = await db + .select() + .from(agentWakeupRequests) + .where(eq(agentWakeupRequests.agentId, agentId)); + expect(requests).toHaveLength(1); + expect(requests[0]).toMatchObject({ + companyId, + source: "timer", + reason: "heartbeat.timer.no_actionable_work", + status: "skipped", + error: null, + }); + + const runs = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.agentId, agentId)); + expect(runs).toHaveLength(0); + }); + it("queues exactly one retry when the recorded local pid is dead", async () => { const { agentId, runId, issueId } = await seedRunFixture({ agentStatus: "idle", diff --git a/server/src/__tests__/issue-stale-execution-lock-routes.test.ts b/server/src/__tests__/issue-stale-execution-lock-routes.test.ts index 1f91fa278b9..944948ec03b 100644 --- a/server/src/__tests__/issue-stale-execution-lock-routes.test.ts +++ b/server/src/__tests__/issue-stale-execution-lock-routes.test.ts @@ -290,6 +290,67 @@ describeEmbeddedPostgres("stale issue execution lock routes", () => { expect(res.body?.error).toBe("Issue run ownership conflict"); }); + it("preserves live checkout ownership on checkout conflicts without retry side effects", async () => { + const { companyId, agentId, currentRunId } = await seedCompanyAgentAndRuns(); + const contenderRunId = randomUUID(); + const issueId = randomUUID(); + await db.insert(heartbeatRuns).values({ + id: contenderRunId, + companyId, + agentId, + status: "running", + invocationSource: "assignment", + startedAt: new Date(), + }); + await db.insert(issues).values({ + id: issueId, + companyId, + title: "Live checkout race", + status: "in_progress", + priority: "high", + assigneeAgentId: agentId, + checkoutRunId: currentRunId, + executionRunId: currentRunId, + executionAgentNameKey: "codexcoder", + executionLockedAt: new Date(), + }); + + const res = await request(createApp(agentActor(companyId, agentId, contenderRunId))) + .post(`/api/issues/${issueId}/checkout`) + .send({ + agentId, + expectedStatuses: ["todo", "backlog", "blocked", "in_review"], + }); + + expect(res.status, JSON.stringify(res.body)).toBe(409); + expect(res.body).toMatchObject({ + error: "Issue checkout conflict", + }); + + const row = await db + .select({ + status: issues.status, + assigneeAgentId: issues.assigneeAgentId, + checkoutRunId: issues.checkoutRunId, + executionRunId: issues.executionRunId, + }) + .from(issues) + .where(eq(issues.id, issueId)) + .then((rows) => rows[0]); + expect(row).toEqual({ + status: "in_progress", + assigneeAgentId: agentId, + checkoutRunId: currentRunId, + executionRunId: currentRunId, + }); + + const checkoutActivity = await db + .select() + .from(activityLog) + .where(eq(activityLog.action, "issue.checked_out")); + expect(checkoutActivity).toHaveLength(0); + }); + it("restricts admin force-release to board users with company access and writes an audit event", async () => { const { companyId, agentId, failedRunId, currentRunId } = await seedCompanyAgentAndRuns(); const issueId = randomUUID();