From a905b6dabcac523c67e56b9884c2311c851da87a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 15:23:50 +0000 Subject: [PATCH] test(perf): merge duplicate one-shot retry regression paths --- src/cron/service.issue-regressions.test.ts | 148 +++++++++------------ 1 file changed, 64 insertions(+), 84 deletions(-) diff --git a/src/cron/service.issue-regressions.test.ts b/src/cron/service.issue-regressions.test.ts index 52c1bc3ae23..58d1c2252c7 100644 --- a/src/cron/service.issue-regressions.test.ts +++ b/src/cron/service.issue-regressions.test.ts @@ -724,50 +724,76 @@ describe("Cron issue regressions", () => { } }); - it("#24355: one-shot job retries on transient error, then succeeds", async () => { - const store = await makeStorePath(); + it("#24355: one-shot retries then succeeds (with and without deleteAfterRun)", async () => { const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); - const cronJob = createIsolatedRegressionJob({ + const runRetryScenario = async (params: { + id: string; + deleteAfterRun: boolean; + }): Promise<{ + state: ReturnType; + runIsolatedAgentJob: ReturnType; + firstRetryAtMs: number; + }> => { + const store = await makeStorePath(); + const cronJob = createIsolatedRegressionJob({ + id: params.id, + name: "reminder", + scheduledAt, + schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, + payload: { kind: "agentTurn", message: "remind me" }, + state: { nextRunAtMs: scheduledAt }, + }); + cronJob.deleteAfterRun = params.deleteAfterRun; + await writeCronJobs(store.storePath, [cronJob]); + + let now = scheduledAt; + const runIsolatedAgentJob = vi + .fn() + .mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" }) + .mockResolvedValueOnce({ status: "ok", summary: "done" }); + const state = createCronServiceState({ + cronEnabled: true, + storePath: store.storePath, + log: noopLogger, + nowMs: () => now, + enqueueSystemEvent: vi.fn(), + requestHeartbeatNow: vi.fn(), + runIsolatedAgentJob, + }); + + await onTimer(state); + const jobAfterRetry = state.store?.jobs.find((j) => j.id === params.id); + expect(jobAfterRetry).toBeDefined(); + expect(jobAfterRetry!.enabled).toBe(true); + expect(jobAfterRetry!.state.lastStatus).toBe("error"); + expect(jobAfterRetry!.state.nextRunAtMs).toBeDefined(); + expect(jobAfterRetry!.state.nextRunAtMs).toBeGreaterThan(scheduledAt); + + const firstRetryAtMs = (jobAfterRetry!.state.nextRunAtMs ?? 0) + 1; + now = firstRetryAtMs; + await onTimer(state); + return { state, runIsolatedAgentJob, firstRetryAtMs }; + }; + + const keepResult = await runRetryScenario({ id: "oneshot-retry", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, + deleteAfterRun: false, }); - cronJob.deleteAfterRun = false; - await writeCronJobs(store.storePath, [cronJob]); + const keepJob = keepResult.state.store?.jobs.find((j) => j.id === "oneshot-retry"); + expect(keepJob).toBeDefined(); + expect(keepJob!.state.lastStatus).toBe("ok"); + expect(keepResult.runIsolatedAgentJob).toHaveBeenCalledTimes(2); - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeatNow: vi.fn(), - runIsolatedAgentJob, + const deleteResult = await runRetryScenario({ + id: "oneshot-deleteAfterRun-retry", + deleteAfterRun: true, }); - - await onTimer(state); - let job = state.store?.jobs.find((j) => j.id === "oneshot-retry"); - expect(job).toBeDefined(); - expect(job!.enabled).toBe(true); - expect(job!.state.lastStatus).toBe("error"); - expect(job!.state.nextRunAtMs).toBeDefined(); - expect(job!.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - - now = (job!.state.nextRunAtMs ?? 0) + 1; - await onTimer(state); - job = state.store?.jobs.find((j) => j.id === "oneshot-retry"); - expect(job).toBeDefined(); - expect(job!.state.lastStatus).toBe("ok"); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); + const deletedJob = deleteResult.state.store?.jobs.find( + (j) => j.id === "oneshot-deleteAfterRun-retry", + ); + expect(deletedJob).toBeUndefined(); + expect(deleteResult.runIsolatedAgentJob).toHaveBeenCalledTimes(2); }); it("#24355: one-shot job disabled after max transient retries", async () => { @@ -896,52 +922,6 @@ describe("Cron issue regressions", () => { expect(job!.state.nextRunAtMs).toBeUndefined(); }); - it("#24355: deleteAfterRun:true one-shot job is deleted after successful retry", async () => { - const store = await makeStorePath(); - const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z"); - - const cronJob = createIsolatedRegressionJob({ - id: "oneshot-deleteAfterRun-retry", - name: "reminder", - scheduledAt, - schedule: { kind: "at", at: new Date(scheduledAt).toISOString() }, - payload: { kind: "agentTurn", message: "remind me" }, - state: { nextRunAtMs: scheduledAt }, - }); - cronJob.deleteAfterRun = true; - await writeCronJobs(store.storePath, [cronJob]); - - let now = scheduledAt; - const runIsolatedAgentJob = vi - .fn() - .mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" }) - .mockResolvedValueOnce({ status: "ok", summary: "done" }); - const state = createCronServiceState({ - cronEnabled: true, - storePath: store.storePath, - log: noopLogger, - nowMs: () => now, - enqueueSystemEvent: vi.fn(), - requestHeartbeatNow: vi.fn(), - runIsolatedAgentJob, - }); - - // First run: transient error → retry scheduled, job still in store. - await onTimer(state); - let job = state.store?.jobs.find((j) => j.id === "oneshot-deleteAfterRun-retry"); - expect(job).toBeDefined(); - expect(job!.enabled).toBe(true); - expect(job!.state.lastStatus).toBe("error"); - expect(job!.state.nextRunAtMs).toBeGreaterThan(scheduledAt); - - // Second run: success → deleteAfterRun removes the job from the store. - now = (job!.state.nextRunAtMs ?? 0) + 1; - await onTimer(state); - const deleted = state.store?.jobs.find((j) => j.id === "oneshot-deleteAfterRun-retry"); - expect(deleted).toBeUndefined(); - expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2); - }); - it("prevents spin loop when cron job completes within the scheduled second (#17821)", async () => { const store = await makeStorePath(); // Simulate a cron job "0 13 * * *" (daily 13:00 UTC) that fires exactly