@@ -550,6 +550,49 @@ describe("cron service timer regressions", () => {
|
550 | 550 | expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); |
551 | 551 | }); |
552 | 552 | |
| 553 | +it("retries recurring jobs after isolated setup timeouts before the next scheduled slot", async () => { |
| 554 | +const store = timerRegressionFixtures.makeStorePath(); |
| 555 | +const scheduledAt = Date.parse("2026-06-08T13:00:00.000Z"); |
| 556 | +const everySixHoursMs = 6 * 60 * 60 * 1_000; |
| 557 | + |
| 558 | +const cronJob = createIsolatedRegressionJob({ |
| 559 | +id: "recurring-setup-timeout-retry", |
| 560 | +name: "ShadowTrader Auto Channel Bug Monitor", |
| 561 | + scheduledAt, |
| 562 | +schedule: { kind: "every", everyMs: everySixHoursMs, anchorMs: scheduledAt }, |
| 563 | +payload: { kind: "agentTurn", message: "monitor bugs" }, |
| 564 | +state: { nextRunAtMs: scheduledAt }, |
| 565 | +}); |
| 566 | +await saveCronStore(store.storePath, { version: 1, jobs: [cronJob] }); |
| 567 | + |
| 568 | +const now = scheduledAt; |
| 569 | +const runIsolatedAgentJob = vi.fn().mockResolvedValueOnce({ |
| 570 | +status: "error", |
| 571 | +error: "cron: isolated agent setup timed out before runner start", |
| 572 | +}); |
| 573 | +const state = createCronServiceState({ |
| 574 | +cronEnabled: true, |
| 575 | +storePath: store.storePath, |
| 576 | +log: noopLogger, |
| 577 | +nowMs: () => now, |
| 578 | +enqueueSystemEvent: vi.fn(), |
| 579 | +requestHeartbeat: vi.fn(), |
| 580 | + runIsolatedAgentJob, |
| 581 | +cronConfig: { |
| 582 | +retry: { maxAttempts: 1, backoffMs: [1000], retryOn: ["timeout"] }, |
| 583 | +}, |
| 584 | +}); |
| 585 | + |
| 586 | +await onTimer(state); |
| 587 | +const jobAfterRetry = requireJob(state, "recurring-setup-timeout-retry"); |
| 588 | +expect(jobAfterRetry.enabled).toBe(true); |
| 589 | +expect(jobAfterRetry.state.lastStatus).toBe("error"); |
| 590 | +expect(jobAfterRetry.state.lastError).toContain("setup timed out before runner start"); |
| 591 | +expect(jobAfterRetry.state.nextRunAtMs).toBeGreaterThan(scheduledAt); |
| 592 | +expect(jobAfterRetry.state.nextRunAtMs).toBeLessThan(scheduledAt + everySixHoursMs); |
| 593 | +expect(runIsolatedAgentJob).toHaveBeenCalledTimes(1); |
| 594 | +}); |
| 595 | + |
553 | 596 | it("uses the normal recurring schedule after transient retry attempts are exhausted", async () => { |
554 | 597 | const store = timerRegressionFixtures.makeStorePath(); |
555 | 598 | const scheduledAt = Date.parse("2026-05-29T02:28:00.000Z"); |
|