diff --git a/server/src/__tests__/heartbeat-workspace-busy.test.ts b/server/src/__tests__/heartbeat-workspace-busy.test.ts index 15ab976c55..1b6ec47da7 100644 --- a/server/src/__tests__/heartbeat-workspace-busy.test.ts +++ b/server/src/__tests__/heartbeat-workspace-busy.test.ts @@ -176,6 +176,29 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { return await heartbeat.getRun(runId); } + // A deferral does two writes in order: first it cancels the original run, + // then it inserts the scheduled-retry row. waitForRunToLeaveActiveStates + // returns after the first write, so a read of the retry row can land before + // the second write and find nothing. Poll until the retry row exists so the + // retry-row assertions never observe the gap between the two writes. + async function waitForRetryRun(originalRunId: string, timeoutMs = 10_000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const retryRun = await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.retryOfRunId, originalRunId)) + .then((rows) => rows[0] ?? null); + if (retryRun) return retryRun; + await new Promise((resolve) => setTimeout(resolve, 50)); + } + return await db + .select() + .from(heartbeatRuns) + .where(eq(heartbeatRuns.retryOfRunId, originalRunId)) + .then((rows) => rows[0] ?? null); + } + interface WorkspaceFixture { companyId: string; projectId: string; @@ -491,11 +514,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { // The deferred run's adapter never executed — the whole point of the gate. expect(executedRunIds).not.toContain(run!.id); - const retryRun = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.retryOfRunId, run!.id)) - .then((rows) => rows[0] ?? null); + const retryRun = await waitForRetryRun(run!.id); expect(retryRun).toMatchObject({ status: "scheduled_retry", scheduledRetryAttempt: 1, @@ -560,11 +579,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { const deferred = await waitForRunToLeaveActiveStates(run!.id); expect(deferred?.status).toBe("cancelled"); - const retryRun = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.retryOfRunId, run!.id)) - .then((rows) => rows[0] ?? null); + const retryRun = await waitForRetryRun(run!.id); expect(retryRun?.status).toBe("scheduled_retry"); // Holder finishes; the due retry promotes, queues, and executes. @@ -606,11 +621,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { expect(deferred?.errorCode).toBe(WORKSPACE_BUSY_ERROR_CODE); expect(executedRunIds).not.toContain(run!.id); - const retryRun = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.retryOfRunId, run!.id)) - .then((rows) => rows[0] ?? null); + const retryRun = await waitForRetryRun(run!.id); expect(retryRun).toMatchObject({ status: "scheduled_retry", scheduledRetryReason: WORKSPACE_BUSY_RETRY_REASON, @@ -658,11 +669,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { const deferred = await waitForRunToLeaveActiveStates(run!.id); expect(deferred?.status).toBe("cancelled"); - const retryRun = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.retryOfRunId, run!.id)) - .then((rows) => rows[0] ?? null); + const retryRun = await waitForRetryRun(run!.id); expect(retryRun?.status).toBe("scheduled_retry"); expect( (retryRun?.contextSnapshot as Record | null)?.workspaceBusyDeferredWhileAssignee, @@ -849,11 +856,7 @@ describeEmbeddedPostgres("shared-workspace run serialization", () => { expect(finishedRun?.errorCode).toBe(WORKSPACE_BUSY_ERROR_CODE); expect(executedRunIds).not.toContain(retryRunId); - const nextRetry = await db - .select() - .from(heartbeatRuns) - .where(eq(heartbeatRuns.retryOfRunId, retryRunId)) - .then((rows) => rows[0] ?? null); + const nextRetry = await waitForRetryRun(retryRunId); expect(nextRetry).toMatchObject({ status: "scheduled_retry", scheduledRetryAttempt: priorAttempts + 1,