diff --git a/ui/src/lib/cron/index.test.ts b/ui/src/lib/cron/index.test.ts index b8e975431f70..5ad066cad535 100644 --- a/ui/src/lib/cron/index.test.ts +++ b/ui/src/lib/cron/index.test.ts @@ -53,6 +53,10 @@ function createCronRequest(jobId: string, options: { existing?: boolean } = {}) }); } +function createMethodRequest(responses: Readonly>) { + return vi.fn(async (method: string) => responses[method] ?? {}); +} + function createCronJob(overrides: Partial & Pick): CronJob { return { enabled: true, @@ -78,6 +82,55 @@ function findRequestCall( return call; } +function createStateWithRequest(request: unknown, overrides: Partial = {}): CronState { + return createState({ + client: { request } as unknown as CronState["client"], + ...overrides, + }); +} + +function createCronForm(overrides: Partial = {}): CronState["cronForm"] { + return { ...DEFAULT_CRON_FORM, ...overrides }; +} + +function createCronSubmitHarness( + jobId: string, + options: { + method?: "cron.add" | "cron.update"; + listExisting?: boolean; + jobs?: CronJob[]; + form?: Partial; + state?: Partial; + } = {}, +) { + const method = options.method ?? "cron.add"; + const request = createCronRequest(jobId, { + existing: options.listExisting ?? method === "cron.update", + }); + const state = createStateWithRequest(request, { + ...options.state, + ...(options.jobs ? { cronJobs: options.jobs } : {}), + cronEditingJobId: method === "cron.update" ? jobId : null, + cronForm: createCronForm(options.form), + }); + const submit = async () => { + const result = await addCronJob(state); + return { call: findRequestCall(request.mock.calls, method), result }; + }; + return { state, submit }; +} + +function createCronEditHarness(job: CronJob) { + const request = createCronRequest(job.id, { existing: true }); + const state = createStateWithRequest(request, { cronJobs: [job] }); + startCronEdit(state, job); + const submit = async () => { + await addCronJob(state); + return findRequestCall(request.mock.calls, "cron.update"); + }; + return { state, submit }; +} + function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`expected ${label} to be a record`); @@ -114,6 +167,10 @@ type EmptyCronListResponse = { nextOffset: null; }; +function emptyCronListResponse(): EmptyCronListResponse { + return { jobs: [], total: 0, hasMore: false, nextOffset: null }; +} + function createDeferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; @@ -124,6 +181,49 @@ function createDeferred() { return { promise, resolve, reject }; } +function createCronRunsResult( + entries: CronRunsResult["entries"], + overrides: Partial> = {}, +): CronRunsResult { + return { + entries, + total: entries.length, + hasMore: false, + nextOffset: null, + ...overrides, + }; +} + +function createCronRunsRace( + currentEntries: CronRunsResult["entries"], + stateOverrides: Partial = {}, +) { + const older = createDeferred(); + const request = vi + .fn() + .mockImplementationOnce(() => older.promise) + .mockResolvedValueOnce(createCronRunsResult(currentEntries)); + return { older, state: createStateWithRequest(request, stateOverrides) }; +} + +function createCronJobsReloadHarness(stateOverrides: Partial = {}) { + const first = createDeferred(); + const payloads: unknown[] = []; + const request = vi.fn(async (method: string, payload?: unknown) => { + if (method !== "cron.list") { + return {}; + } + payloads.push(payload); + return payloads.length === 1 ? first.promise : emptyCronListResponse(); + }); + return { + first, + payloads, + request, + state: createStateWithRequest(request, stateOverrides), + }; +} + describe("cron controller", () => { it("collects configured model suggestions from defaults and per-agent entries", () => { expect( @@ -213,88 +313,65 @@ describe("cron controller", () => { it.each([ ["cron.add", null], ["cron.update", "no-timeout-job"], - ] as const)("preserves an explicit zero timeout in %s payloads", async (method, editingJobId) => { - const request = vi.fn(async (requestedMethod: string) => { - if (requestedMethod === method) { - return { id: editingJobId ?? "no-timeout-job" }; - } - if (requestedMethod === "cron.list") { - return { jobs: [] }; - } - if (requestedMethod === "cron.status") { - return { enabled: true, jobs: 0, nextWakeAtMs: null }; - } - return {}; - }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: editingJobId, - cronForm: { - ...DEFAULT_CRON_FORM, - name: "No timeout", - payloadText: "Run until complete", - timeoutSeconds: "0", - }, - }); + ] as const)( + "preserves an explicit zero timeout in %s payloads", + async (method, _editingJobId) => { + const { submit } = createCronSubmitHarness("no-timeout-job", { + method, + listExisting: false, + form: { + name: "No timeout", + payloadText: "Run until complete", + timeoutSeconds: "0", + }, + }); - expect(await addCronJob(state)).toEqual({ saved: true, jobId: "no-timeout-job" }); + const submitted = await submit(); + expect(submitted.result).toEqual({ saved: true, jobId: "no-timeout-job" }); - const call = findRequestCall(request.mock.calls, method); - const job = method === "cron.update" ? requestPatch(call) : requestPayload(call); - expectNestedRecordFields(job, "payload", { - kind: "agentTurn", - message: "Run until complete", - timeoutSeconds: 0, - }); - }); + const call = submitted.call; + const job = method === "cron.update" ? requestPatch(call) : requestPayload(call); + expectNestedRecordFields(job, "payload", { + kind: "agentTurn", + message: "Run until complete", + timeoutSeconds: 0, + }); + }, + ); it.each(["", " "])("omits an inherited timeout from cron.add: %j", async (timeoutSeconds) => { - const request = createCronRequest("inherited-timeout-job"); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("inherited-timeout-job", { + form: { name: "Inherited timeout", payloadText: "Use the default timeout", timeoutSeconds, }, }); - expect(await addCronJob(state)).toEqual({ saved: true, jobId: "inherited-timeout-job" }); - const payload = requireRecord( - requestPayload(findRequestCall(request.mock.calls, "cron.add")).payload, - "cron.add agent payload", - ); + const submitted = await submit(); + expect(submitted.result).toEqual({ saved: true, jobId: "inherited-timeout-job" }); + const payload = requireRecord(requestPayload(submitted.call).payload, "cron.add agent payload"); expect(payload).not.toHaveProperty("timeoutSeconds"); }); it("forwards webhook delivery in cron.add payload", async () => { - const request = createCronRequest("job-1"); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-1", { + form: { name: "webhook job", scheduleKind: "every", everyAmount: "1", everyUnit: "minutes", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "run this", deliveryMode: "webhook", deliveryTo: "https://example.invalid/cron", }, }); - const saved = await addCronJob(state); + const submitted = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expect(saved.saved).toBe(true); - const payload = requestPayload(addCall); + expect(submitted.result.saved).toBe(true); + const payload = requestPayload(submitted.call); expectRecordFields(payload, { name: "webhook job", }); @@ -307,29 +384,18 @@ describe("cron controller", () => { it("returns the saved job id from both cron.add response shapes", async () => { const responses = [{ created: true, job: { id: "job-wrapped" } }, { id: "job-bare" }]; for (const response of responses) { - const request = vi.fn(async (method: string) => { - if (method === "cron.add") { - return response; - } - if (method === "cron.list") { - return { jobs: [] }; - } - if (method === "cron.status") { - return { enabled: true, jobs: 0, nextWakeAtMs: null }; - } - return {}; + const request = createMethodRequest({ + "cron.add": response, + "cron.list": { jobs: [] }, + "cron.status": { enabled: true, jobs: 0, nextWakeAtMs: null }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const state = createStateWithRequest(request, { + cronForm: createCronForm({ name: "id echo", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", - payloadKind: "agentTurn", payloadText: "run this", - }, + }), }); const saved = await addCronJob(state); @@ -342,17 +408,11 @@ describe("cron controller", () => { }); it("forwards sessionKey and delivery accountId in cron.add payload", async () => { - const request = createCronRequest("job-3"); - - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-3", { + form: { name: "account-routed", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", - payloadKind: "agentTurn", payloadText: "run this", sessionKey: "agent:ops:main", deliveryMode: "announce", @@ -360,10 +420,9 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - const payload = requestPayload(addCall); + const payload = requestPayload(call); expectRecordFields(payload, { sessionKey: "agent:ops:main", }); @@ -374,154 +433,105 @@ describe("cron controller", () => { }); it("omits a blank delivery accountId from cron.add payloads", async () => { - const request = createCronRequest("job-blank-account-id"); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-blank-account-id", { + form: { name: "implicit account", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", - payloadKind: "agentTurn", payloadText: "run this", deliveryMode: "announce", deliveryAccountId: " ", }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expect(requireRecord(requestPayload(addCall).delivery, "delivery").accountId).toBeUndefined(); + expect(requireRecord(requestPayload(call).delivery, "delivery").accountId).toBeUndefined(); }); it('omits delivery.channel when the form still uses the "last" sentinel', async () => { - const request = createCronRequest("job-last-add"); - - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-last-add", { + form: { name: "implicit channel", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "run this", deliveryMode: "announce", deliveryChannel: "last", }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expectRecordFields(requireRecord(requestPayload(addCall).delivery, "delivery"), { + expectRecordFields(requireRecord(requestPayload(call).delivery, "delivery"), { mode: "announce", }); expect( - (addCall[1] as { delivery?: { channel?: string } } | undefined)?.delivery?.channel, + (call[1] as { delivery?: { channel?: string } } | undefined)?.delivery?.channel, ).toBeUndefined(); }); it("forwards lightContext in cron payload", async () => { - const request = createCronRequest("job-light"); - - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-light", { + form: { name: "light-context job", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", - payloadKind: "agentTurn", payloadText: "run this", payloadLightContext: true, }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expectNestedRecordFields(requestPayload(addCall), "payload", { + expectNestedRecordFields(requestPayload(call), "payload", { kind: "agentTurn", lightContext: true, }); }); it('sends delivery: { mode: "none" } explicitly in cron.add payload', async () => { - const request = createCronRequest("job-none-add"); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-none-add", { + form: { name: "none delivery job", - scheduleKind: "every", everyAmount: "1", everyUnit: "minutes", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "run this", deliveryMode: "none", }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expect((addCall[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({ + expect((call[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({ mode: "none", }); }); it('sends delivery: { mode: "none" } explicitly in cron.update patch', async () => { - const request = createCronRequest("job-none-update", { existing: true }); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronEditingJobId: "job-none-update", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-none-update", { + method: "cron.update", + form: { name: "switch to none", - scheduleKind: "every", - everyAmount: "30", - everyUnit: "minutes", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "do work", deliveryMode: "none", }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expect( - (updateCall[1] as { patch?: { delivery?: unknown } } | undefined)?.patch?.delivery, - ).toEqual({ + expect((call[1] as { patch?: { delivery?: unknown } } | undefined)?.patch?.delivery).toEqual({ mode: "none", }); }); it("sends explicit null model/thinking clears when blanking stored overrides on edit", async () => { - const request = createCronRequest("job-clear-overrides", { existing: true }); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronEditingJobId: "job-clear-overrides", - cronJobs: [ + const { submit } = createCronSubmitHarness("job-clear-overrides", { + method: "cron.update", + jobs: [ { id: "job-clear-overrides", payload: { @@ -532,25 +542,18 @@ describe("cron controller", () => { }, } as unknown as CronState["cronJobs"][number], ], - cronForm: { - ...DEFAULT_CRON_FORM, + form: { name: "clear overrides", - scheduleKind: "every", - everyAmount: "30", - everyUnit: "minutes", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "do work", payloadModel: "", payloadThinking: "", }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectNestedRecordFields(requestPatch(updateCall), "payload", { + expectNestedRecordFields(requestPatch(call), "payload", { kind: "agentTurn", message: "do work", model: null, @@ -559,33 +562,22 @@ describe("cron controller", () => { }); it("does not send null model/thinking for a new job with blank fields", async () => { - const request = createCronRequest("job-new-blank", { existing: true }); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-new-blank", { + listExisting: true, + form: { name: "new blank", - scheduleKind: "every", - everyAmount: "30", - everyUnit: "minutes", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "do work", payloadModel: "", payloadThinking: "", }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); // A new job never had a stored override, so a blank field stays omitted // (no explicit null clear) rather than being mistaken for a cleared value. - expectNestedRecordFields(requestPayload(addCall), "payload", { + expectNestedRecordFields(requestPayload(call), "payload", { kind: "agentTurn", message: "do work", model: undefined, @@ -594,18 +586,10 @@ describe("cron controller", () => { }); it("does not submit stale announce delivery when unsupported", async () => { - const request = createCronRequest("job-2"); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { state, submit } = createCronSubmitHarness("job-2", { + form: { name: "main job", - scheduleKind: "every", everyAmount: "1", - everyUnit: "minutes", sessionTarget: "main", wakeMode: "next-heartbeat", payloadKind: "systemEvent", @@ -615,15 +599,14 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expectRecordFields(requestPayload(addCall), { + expectRecordFields(requestPayload(call), { name: "main job", }); // Delivery is explicitly sent as { mode: "none" } to clear the announce delivery on the backend. // Previously this was sent as undefined, which left announce in place (bug #31075). - expect((addCall[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({ + expect((call[1] as { delivery?: unknown } | undefined)?.delivery).toEqual({ mode: "none", }); // After submit, form is reset to defaults (deliveryMode = "announce" from DEFAULT_CRON_FORM). @@ -631,15 +614,9 @@ describe("cron controller", () => { }); it("submits cron.update when editing an existing job", async () => { - const request = createCronRequest("job-1", { existing: true }); - - const state = createState({ - client: { - request, - } as unknown as CronState["client"], - cronEditingJobId: "job-1", - cronForm: { - ...DEFAULT_CRON_FORM, + const { state, submit } = createCronSubmitHarness("job-1", { + method: "cron.update", + form: { name: "edited job", description: "", clearAgent: true, @@ -653,13 +630,12 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-1", }); - expectRecordFields(requestPatch(updateCall), { + expectRecordFields(requestPatch(call), { name: "edited job", description: "", agentId: null, @@ -667,52 +643,36 @@ describe("cron controller", () => { payload: { kind: "systemEvent", text: "updated" }, delivery: { mode: "none" }, }); - expect(requestPatch(updateCall)).not.toHaveProperty("deleteAfterRun"); + expect(requestPatch(call)).not.toHaveProperty("deleteAfterRun"); expect(state.cronEditingJobId).toBeNull(); }); it("sends null delivery.accountId in cron.update to clear persisted account routing", async () => { - const request = createCronRequest("job-clear-account-id", { existing: true }); - - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-clear-account-id", - cronJobs: [ - { - id: "job-clear-account-id", - name: "clear account", - enabled: true, - createdAtMs: 0, - updatedAtMs: 0, - schedule: { kind: "cron", expr: "0 * * * *" }, - sessionTarget: "isolated", - wakeMode: "next-heartbeat", - payload: { kind: "agentTurn", message: "run" }, - delivery: { mode: "announce", accountId: "ops-bot" }, - state: {}, - }, - ], - cronForm: { - ...DEFAULT_CRON_FORM, + const job = createCronJob({ + id: "job-clear-account-id", + name: "clear account", + delivery: { mode: "announce", accountId: "ops-bot" }, + }); + const { submit } = createCronSubmitHarness(job.id, { + method: "cron.update", + jobs: [job], + form: { name: "clear account", scheduleKind: "cron", cronExpr: "0 * * * *", - sessionTarget: "isolated", wakeMode: "next-heartbeat", - payloadKind: "agentTurn", payloadText: "run", deliveryMode: "announce", deliveryAccountId: " ", }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-clear-account-id", }); - expectRecordFields(requireRecord(requestPatch(updateCall).delivery, "delivery"), { + expectRecordFields(requireRecord(requestPatch(call).delivery, "delivery"), { mode: "announce", accountId: null, }); @@ -766,7 +726,6 @@ describe("cron controller", () => { }); it("preserves command payloads when editing Control UI metadata", async () => { - const request = createCronRequest("job-command", { existing: true }); const job = createCronJob({ id: "job-command", name: "Command", @@ -774,54 +733,35 @@ describe("cron controller", () => { payload: { kind: "command", argv: ["sh", "-lc", "echo ok"] }, delivery: { mode: "announce", channel: "telegram", to: "123" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.name = "Command renamed"; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - const patch = requestPatch(updateCall); + const patch = requestPatch(call); expect(patch.name).toBe("Command renamed"); expect(patch).not.toHaveProperty("payload"); }); it("loads and preserves script payloads as read-only metadata edits", async () => { - const scriptJob = { + const script = "const result = await agent('check status')"; + const scriptJob = createCronJob({ id: "job-script", name: "Script", - enabled: true, - createdAtMs: 0, - updatedAtMs: 0, - schedule: { kind: "every" as const, everyMs: 600_000 }, - sessionTarget: "isolated" as const, - wakeMode: "next-heartbeat" as const, + schedule: { kind: "every", everyMs: 600_000 }, payload: { - kind: "script" as const, - script: "const result = await agent('check status')", + kind: "script", + script, toolBudget: 4, }, - delivery: { mode: "none" as const }, - state: {}, - }; - const request = vi.fn(async (method: string) => { - if (method === "cron.list") { - return { jobs: [scriptJob], total: 1, hasMore: false, nextOffset: null }; - } - if (method === "cron.update") { - return { id: scriptJob.id }; - } - if (method === "cron.status") { - return { enabled: true, jobs: 1, nextWakeAtMs: null }; - } - return {}; + delivery: { mode: "none" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const request = createMethodRequest({ + "cron.list": { jobs: [scriptJob], total: 1, hasMore: false, nextOffset: null }, + "cron.update": { id: scriptJob.id }, + "cron.status": { enabled: true, jobs: 1, nextWakeAtMs: null }, }); + const state = createStateWithRequest(request); await loadCronJobsPage(state); expect(state.cronJobs).toEqual([scriptJob]); @@ -829,7 +769,7 @@ describe("cron controller", () => { startCronEdit(state, scriptJob); expect(state.cronForm.payloadKind).toBe("script"); expect(state.cronForm.payloadLocked).toBe(true); - expect(state.cronForm.payloadText).toBe(scriptJob.payload.script); + expect(state.cronForm.payloadText).toBe(script); state.cronForm.name = "Script renamed"; await addCronJob(state); @@ -840,7 +780,6 @@ describe("cron controller", () => { }); it("preserves on-exit schedules when editing Control UI metadata", async () => { - const request = createCronRequest("job-on-exit", { existing: true }); const job = createCronJob({ id: "job-on-exit", name: "On exit", @@ -848,25 +787,19 @@ describe("cron controller", () => { payload: { kind: "agentTurn", message: "report" }, delivery: { mode: "none" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.name = "On exit renamed"; state.cronForm.cronExpr = ""; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - const patch = requestPatch(updateCall); + const patch = requestPatch(call); expect(patch.name).toBe("On exit renamed"); expect(patch).not.toHaveProperty("schedule"); expect(state.cronFieldErrors).toEqual({}); }); it("preserves stream schedules when editing Control UI metadata", async () => { - const request = createCronRequest("job-stream", { existing: true }); const job = createCronJob({ id: "job-stream", name: "Stream", @@ -874,23 +807,18 @@ describe("cron controller", () => { payload: { kind: "agentTurn", message: "report" }, delivery: { mode: "none" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.name = "Stream renamed"; - await addCronJob(state); + const call = await submit(); - const patch = requestPatch(findRequestCall(request.mock.calls, "cron.update")); + const patch = requestPatch(call); expect(patch.name).toBe("Stream renamed"); expect(patch).not.toHaveProperty("schedule"); expect(state.cronFieldErrors).toEqual({}); }); it("applies schedule edits when changing an on-exit job to a regular schedule", async () => { - const request = createCronRequest("job-on-exit", { existing: true }); const job = createCronJob({ id: "job-on-exit", name: "On exit", @@ -898,81 +826,61 @@ describe("cron controller", () => { payload: { kind: "agentTurn", message: "report" }, delivery: { mode: "none" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.scheduleKind = "every"; state.cronForm.everyAmount = "5"; state.cronForm.everyUnit = "minutes"; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - const patch = requestPatch(updateCall); + const patch = requestPatch(call); expect(patch.schedule).toEqual({ kind: "every", everyMs: 300_000 }); }); it('keeps implicit announce delivery implicit when editing a job that shows "last" in the form', async () => { - const request = createCronRequest("job-implicit-delivery", { existing: true }); const job = createCronJob({ id: "job-implicit-delivery", name: "Implicit delivery", delivery: { mode: "announce", to: "123" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { submit } = createCronEditHarness(job); - startCronEdit(state, job); - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-implicit-delivery", }); - expectRecordFields(requireRecord(requestPatch(updateCall).delivery, "delivery"), { + expectRecordFields(requireRecord(requestPatch(call).delivery, "delivery"), { mode: "announce", to: "123", }); expect( - (updateCall[1] as { patch?: { delivery?: { channel?: string } } } | undefined)?.patch - ?.delivery?.channel, + (call[1] as { patch?: { delivery?: { channel?: string } } } | undefined)?.patch?.delivery + ?.channel, ).toBeUndefined(); }); it('sends delivery.channel="last" when editing clears an explicit channel back to implicit-last', async () => { - const request = createCronRequest("job-clear-delivery-channel", { existing: true }); const job = createCronJob({ id: "job-clear-delivery-channel", name: "Clear delivery channel", delivery: { mode: "announce", channel: "telegram", to: "123" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.deliveryChannel = "last"; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); expect( - (updateCall[1] as { patch?: { delivery?: { channel?: string } } } | undefined)?.patch - ?.delivery?.channel, + (call[1] as { patch?: { delivery?: { channel?: string } } } | undefined)?.patch?.delivery + ?.channel, ).toBe("last"); }); it("includes model/thinking/stagger/bestEffort in cron.update patch", async () => { - const request = createCronRequest("job-2", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-2", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-2", { + method: "cron.update", + form: { name: "advanced edit", scheduleKind: "cron", cronExpr: "0 9 * * *", @@ -987,13 +895,12 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-2", }); - const patch = requestPatch(updateCall); + const patch = requestPatch(call); expectRecordFields(patch, { schedule: { kind: "cron", expr: "0 9 * * *", staggerMs: 30_000 }, payload: { @@ -1010,26 +917,17 @@ describe("cron controller", () => { }); it("sends lightContext=false in cron.update when clearing prior light-context setting", async () => { - const request = createCronRequest("job-clear-light", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-clear-light", - cronJobs: [ - { - id: "job-clear-light", - name: "Light job", - enabled: true, - createdAtMs: 0, - updatedAtMs: 0, - schedule: { kind: "cron", expr: "0 9 * * *" }, - sessionTarget: "isolated", - wakeMode: "now", - payload: { kind: "agentTurn", message: "run", lightContext: true }, - state: {}, - }, - ], - cronForm: { - ...DEFAULT_CRON_FORM, + const job = createCronJob({ + id: "job-clear-light", + name: "Light job", + schedule: { kind: "cron", expr: "0 9 * * *" }, + wakeMode: "now", + payload: { kind: "agentTurn", message: "run", lightContext: true }, + }); + const { submit } = createCronSubmitHarness(job.id, { + method: "cron.update", + jobs: [job], + form: { name: "Light job", scheduleKind: "cron", cronExpr: "0 9 * * *", @@ -1039,25 +937,21 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-clear-light", }); - expectRecordFields(requireRecord(requestPatch(updateCall).payload, "payload"), { + expectRecordFields(requireRecord(requestPatch(call).payload, "payload"), { kind: "agentTurn", lightContext: false, }); }); it("includes custom failureAlert fields in cron.update patch", async () => { - const request = createCronRequest("job-alert", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-alert", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-alert", { + method: "cron.update", + form: { name: "alert job", payloadKind: "agentTurn", payloadText: "run it", @@ -1069,13 +963,12 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-alert", }); - expectRecordFields(requireRecord(requestPatch(updateCall).failureAlert, "failureAlert"), { + expectRecordFields(requireRecord(requestPatch(call).failureAlert, "failureAlert"), { after: 3, cooldownMs: 120_000, channel: "telegram", @@ -1086,12 +979,9 @@ describe("cron controller", () => { }); it("includes failure alert mode/accountId in cron.update patch", async () => { - const request = createCronRequest("job-alert-mode", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-alert-mode", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-alert-mode", { + method: "cron.update", + form: { name: "alert mode job", payloadKind: "agentTurn", payloadText: "run it", @@ -1102,13 +992,12 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-alert-mode", }); - expectRecordFields(requireRecord(requestPatch(updateCall).failureAlert, "failureAlert"), { + expectRecordFields(requireRecord(requestPatch(call).failureAlert, "failureAlert"), { after: 1, mode: "webhook", accountId: "bot-a", @@ -1116,67 +1005,52 @@ describe("cron controller", () => { }); it('keeps implicit failure alert delivery implicit when editing a job that shows "last" in the form', async () => { - const request = createCronRequest("job-alert-implicit-channel", { existing: true }); const job = createCronJob({ id: "job-alert-implicit-channel", name: "Implicit failure alert", delivery: { mode: "announce", channel: "telegram", to: "123" }, failureAlert: { after: 2, to: "123" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { submit } = createCronEditHarness(job); - startCronEdit(state, job); - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-alert-implicit-channel", }); - expectRecordFields(requireRecord(requestPatch(updateCall).failureAlert, "failureAlert"), { + expectRecordFields(requireRecord(requestPatch(call).failureAlert, "failureAlert"), { after: 2, to: "123", mode: "announce", }); expect( - (updateCall[1] as { patch?: { failureAlert?: { channel?: string } } } | undefined)?.patch + (call[1] as { patch?: { failureAlert?: { channel?: string } } } | undefined)?.patch ?.failureAlert?.channel, ).toBeUndefined(); }); it('sends failureAlert.channel="last" when editing clears an explicit failure channel back to implicit-last', async () => { - const request = createCronRequest("job-clear-failure-channel", { existing: true }); const job = createCronJob({ id: "job-clear-failure-channel", name: "Clear failure channel", delivery: { mode: "announce", channel: "telegram", to: "123" }, failureAlert: { after: 2, channel: "telegram", to: "123" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.failureAlertChannel = "last"; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); expect( - (updateCall[1] as { patch?: { failureAlert?: { channel?: string } } } | undefined)?.patch + (call[1] as { patch?: { failureAlert?: { channel?: string } } } | undefined)?.patch ?.failureAlert?.channel, ).toBe("last"); }); it("omits failureAlert.cooldownMs when custom cooldown is left blank", async () => { - const request = createCronRequest("job-alert-no-cooldown", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-alert-no-cooldown", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-alert-no-cooldown", { + method: "cron.update", + form: { name: "alert job no cooldown", payloadKind: "agentTurn", payloadText: "run it", @@ -1188,25 +1062,22 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-alert-no-cooldown", }); - expectRecordFields(requireRecord(requestPatch(updateCall).failureAlert, "failureAlert"), { + expectRecordFields(requireRecord(requestPatch(call).failureAlert, "failureAlert"), { after: 3, channel: "telegram", to: "123456", }); expect( - (updateCall[1] as { patch?: { failureAlert?: { cooldownMs?: number } } })?.patch - ?.failureAlert, + (call[1] as { patch?: { failureAlert?: { cooldownMs?: number } } })?.patch?.failureAlert, ).not.toHaveProperty("cooldownMs"); }); it("clears persisted failure alert routing fields when their edit inputs are blanked", async () => { - const request = createCronRequest("job-clear-alert-fields", { existing: true }); const job = createCronJob({ id: "job-clear-alert-fields", name: "Clear failure alert fields", @@ -1219,27 +1090,22 @@ describe("cron controller", () => { accountId: "bot-a", }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronJobs: [job], - }); + const { state, submit } = createCronEditHarness(job); - startCronEdit(state, job); state.cronForm.failureAlertAfter = ""; state.cronForm.failureAlertTo = ""; state.cronForm.failureAlertCooldownSeconds = ""; state.cronForm.failureAlertAccountId = ""; - await addCronJob(state); + const call = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requireRecord(requestPatch(updateCall).failureAlert, "failureAlert"), { + expectRecordFields(requireRecord(requestPatch(call).failureAlert, "failureAlert"), { after: null, to: null, cooldownMs: null, accountId: null, }); // oxlint-disable-next-line unicorn/prefer-structured-clone -- verify the websocket JSON wire shape - const serializedPayload = JSON.parse(JSON.stringify(requestPayload(updateCall))) as unknown; + const serializedPayload = JSON.parse(JSON.stringify(requestPayload(call))) as unknown; expectRecordFields( requireRecord( requireRecord(requireRecord(serializedPayload, "payload").patch, "patch").failureAlert, @@ -1250,19 +1116,13 @@ describe("cron controller", () => { }); it("clears a persisted failure alert override when switching back to inherit", async () => { - const request = vi.fn(async (method: string, _payload?: unknown) => { - if (method === "cron.update") { - return { id: "job-inherit-alert" }; - } - return {}; - }); + const request = createMethodRequest({ "cron.update": { id: "job-inherit-alert" } }); const job = createCronJob({ id: "job-inherit-alert", name: "Inherit failure alerts", failureAlert: { after: 2, channel: "telegram" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobs: [job], }); @@ -1275,12 +1135,9 @@ describe("cron controller", () => { }); it("includes failureAlert=false when disabled per job", async () => { - const request = createCronRequest("job-no-alert", { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronEditingJobId: "job-no-alert", - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-no-alert", { + method: "cron.update", + form: { name: "alert off", payloadKind: "agentTurn", payloadText: "run it", @@ -1288,13 +1145,12 @@ describe("cron controller", () => { }, }); - await addCronJob(state); + const { call } = await submit(); - const updateCall = findRequestCall(request.mock.calls, "cron.update"); - expectRecordFields(requestPayload(updateCall), { + expectRecordFields(requestPayload(call), { id: "job-no-alert", }); - expect(requestPatch(updateCall).failureAlert).toBe(false); + expect(requestPatch(call).failureAlert).toBe(false); }); it("maps cron stagger, model, thinking, and best effort into form", () => { @@ -1402,8 +1258,7 @@ describe("cron controller", () => { "rejects invalid recurring amounts before submit: %s", async (everyAmount) => { const request = createCronRequest("job-nondecimal"); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronForm: { ...DEFAULT_CRON_FORM, name: "decimal interval", @@ -1431,11 +1286,8 @@ describe("cron controller", () => { ] as const)( "converts %s %s to safe integer milliseconds", async (everyAmount, everyUnit, expectedEveryMs) => { - const request = createCronRequest("job-decimal"); - const state = createState({ - client: { request } as unknown as CronState["client"], - cronForm: { - ...DEFAULT_CRON_FORM, + const { submit } = createCronSubmitHarness("job-decimal", { + form: { name: "decimal interval", everyAmount, everyUnit, @@ -1444,11 +1296,10 @@ describe("cron controller", () => { }, }); - const saved = await addCronJob(state); + const submitted = await submit(); - expect(saved.saved).toBe(true); - const addCall = findRequestCall(request.mock.calls, "cron.add"); - expect(requestPayload(addCall).schedule).toEqual({ + expect(submitted.result.saved).toBe(true); + expect(requestPayload(submitted.call).schedule).toEqual({ kind: "every", everyMs: expectedEveryMs, }); @@ -1469,8 +1320,7 @@ describe("cron controller", () => { it("blocks add/update submit when validation errors exist", async () => { const request = vi.fn(async () => ({})); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronForm: { ...DEFAULT_CRON_FORM, name: "", @@ -1550,8 +1400,7 @@ describe("cron controller", () => { sessionTarget: "main", payload: { kind: "systemEvent", text: "ping" }, }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobs: [sourceJob], cronAgentId: "main", cronEditingJobId: "job-1", @@ -1648,8 +1497,7 @@ describe("cron controller", () => { } return {}; }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobsQuery: "daily", cronJobsEnabledFilter: "enabled", cronJobsScheduleKindFilter: "cron", @@ -1671,12 +1519,11 @@ describe("cron controller", () => { const listPayload = requireRecord(payload, "cron.list payload"); expect(listPayload).not.toHaveProperty("scheduleKind"); expect(listPayload).not.toHaveProperty("lastRunStatus"); - return { jobs: [], total: 0, hasMore: false, nextOffset: null }; + return emptyCronListResponse(); } return {}; }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobsScheduleKindFilter: "cron", cronJobsLastStatusFilter: "error", }); @@ -1693,26 +1540,7 @@ describe("cron controller", () => { }); it("reloads cron jobs after filters change during an in-flight table load", async () => { - let resolveFirst!: (value: EmptyCronListResponse) => void; - const firstResponse = new Promise((resolve) => { - resolveFirst = resolve; - }); - const request = vi.fn(async (method: string, payload?: unknown) => { - if (method !== "cron.list") { - return {}; - } - if (request.mock.calls.length === 1) { - return firstResponse; - } - expectRecordFields(requireRecord(payload, "pending cron.list payload"), { - scheduleKind: "cron", - lastRunStatus: "unknown", - }); - return { jobs: [], total: 0, hasMore: false, nextOffset: null }; - }); - const state = createState({ - client: { request } as unknown as CronState["client"], - }); + const { first, payloads, request, state } = createCronJobsReloadHarness(); const firstLoad = loadCronJobsPage(state, { tableFilters: true }); updateCronJobsFilter(state, { @@ -1720,38 +1548,20 @@ describe("cron controller", () => { cronJobsLastStatusFilter: "unknown", }); await loadCronJobsPage(state, { tableFilters: true }); - resolveFirst({ jobs: [], total: 0, hasMore: false, nextOffset: null }); + first.resolve(emptyCronListResponse()); await firstLoad; + expectRecordFields(requireRecord(payloads[1], "pending cron.list payload"), { + scheduleKind: "cron", + lastRunStatus: "unknown", + }); expect(request).toHaveBeenCalledTimes(2); expect(state.cronJobsReloadPending).toBe(false); expect(state.cronJobsReloadPendingTableFilters).toBe(false); }); it("reloads cron jobs after filters change during an in-flight append load", async () => { - let resolveAppend!: (value: EmptyCronListResponse) => void; - const appendResponse = new Promise((resolve) => { - resolveAppend = resolve; - }); - const request = vi.fn(async (method: string, payload?: unknown) => { - if (method !== "cron.list") { - return {}; - } - if (request.mock.calls.length === 1) { - expectRecordFields(requireRecord(payload, "append cron.list payload"), { - offset: 1, - }); - return appendResponse; - } - expectRecordFields(requireRecord(payload, "pending append cron.list payload"), { - offset: 0, - scheduleKind: "cron", - lastRunStatus: "unknown", - }); - return { jobs: [], total: 0, hasMore: false, nextOffset: null }; - }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const { first, payloads, request, state } = createCronJobsReloadHarness({ cronJobs: [ createCronJob({ id: "existing", @@ -1771,33 +1581,24 @@ describe("cron controller", () => { cronJobsLastStatusFilter: "unknown", }); await loadCronJobsPage(state, { tableFilters: true }); - resolveAppend({ jobs: [], total: 0, hasMore: false, nextOffset: null }); + first.resolve(emptyCronListResponse()); await appendLoad; + expectRecordFields(requireRecord(payloads[0], "append cron.list payload"), { + offset: 1, + }); + expectRecordFields(requireRecord(payloads[1], "pending append cron.list payload"), { + offset: 0, + scheduleKind: "cron", + lastRunStatus: "unknown", + }); expect(request).toHaveBeenCalledTimes(2); expect(state.cronJobsReloadPending).toBe(false); expect(state.cronJobsReloadPendingTableFilters).toBe(false); }); it("uses the latest queued cron jobs table-filter mode", async () => { - let resolveFirst!: (value: EmptyCronListResponse) => void; - const firstResponse = new Promise((resolve) => { - resolveFirst = resolve; - }); - const request = vi.fn(async (method: string, payload?: unknown) => { - if (method !== "cron.list") { - return {}; - } - if (request.mock.calls.length === 1) { - return firstResponse; - } - const pendingPayload = requireRecord(payload, "latest pending cron.list payload"); - expect(pendingPayload).not.toHaveProperty("scheduleKind"); - expect(pendingPayload).not.toHaveProperty("lastRunStatus"); - return { jobs: [], total: 0, hasMore: false, nextOffset: null }; - }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const { first, payloads, request, state } = createCronJobsReloadHarness({ cronJobsScheduleKindFilter: "cron", cronJobsLastStatusFilter: "unknown", }); @@ -1805,9 +1606,12 @@ describe("cron controller", () => { const firstLoad = loadCronJobsPage(state); await loadCronJobsPage(state, { tableFilters: true }); await loadCronJobsPage(state); - resolveFirst({ jobs: [], total: 0, hasMore: false, nextOffset: null }); + first.resolve(emptyCronListResponse()); await firstLoad; + const pendingPayload = requireRecord(payloads[1], "latest pending cron.list payload"); + expect(pendingPayload).not.toHaveProperty("scheduleKind"); + expect(pendingPayload).not.toHaveProperty("lastRunStatus"); expect(request).toHaveBeenCalledTimes(2); expect(state.cronJobsReloadPending).toBe(false); expect(state.cronJobsReloadPendingTableFilters).toBe(false); @@ -1838,9 +1642,7 @@ describe("cron controller", () => { } return {}; }); - const state = createState({ - client: { request } as unknown as CronState["client"], - }); + const state = createStateWithRequest(request); await loadCronJobsPage(state); @@ -1870,9 +1672,7 @@ describe("cron controller", () => { nextOffset: null, }; }); - const state = createState({ - client: { request } as unknown as CronState["client"], - }); + const state = createStateWithRequest(request); await expect(loadCronRuns(state, "job-1")).resolves.toBe("ok"); expect(state.cronRuns).toHaveLength(1); @@ -1885,30 +1685,21 @@ describe("cron controller", () => { }); it("keeps the newest filtered run history when an older overview request finishes last", async () => { - const olderOverview = createDeferred(); const currentEntry = { ts: 2, jobId: "fresh-job", status: "ok" as const, summary: "fresh" }; - const request = vi - .fn() - .mockImplementationOnce(() => olderOverview.promise) - .mockResolvedValueOnce({ - entries: [currentEntry], - total: 1, - hasMore: false, - nextOffset: null, - }); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const { older: olderOverview, state } = createCronRunsRace([currentEntry]); const olderLoad = loadCronRuns(state, null); updateCronRunsFilter(state, { cronRunsQuery: "fresh" }); await expect(loadCronRuns(state, null)).resolves.toBe("ok"); expect(state.cronRuns).toEqual([currentEntry]); - olderOverview.resolve({ - entries: [{ ts: 1, jobId: "stale-job", status: "ok", summary: "stale" }], - total: 8, - hasMore: true, - nextOffset: 1, - }); + olderOverview.resolve( + createCronRunsResult([{ ts: 1, jobId: "stale-job", status: "ok", summary: "stale" }], { + total: 8, + hasMore: true, + nextOffset: 1, + }), + ); await expect(olderLoad).resolves.toBe("skipped"); expect(state.cronRuns).toEqual([currentEntry]); @@ -1918,35 +1709,22 @@ describe("cron controller", () => { }); it("does not let a deferred overview replace a newly selected job's run history", async () => { - const olderOverview = createDeferred(); const selectedEntry = { ts: 2, jobId: "selected-job", status: "ok" as const, summary: "selected history", }; - const request = vi - .fn() - .mockImplementationOnce(() => olderOverview.promise) - .mockResolvedValueOnce({ - entries: [selectedEntry], - total: 1, - hasMore: false, - nextOffset: null, - }); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const { older: olderOverview, state } = createCronRunsRace([selectedEntry]); const olderLoad = loadCronRuns(state, null); updateCronRunsFilter(state, { cronRunsScope: "job" }); state.cronRunsJobId = "selected-job"; await expect(loadCronRuns(state, "selected-job")).resolves.toBe("ok"); - olderOverview.resolve({ - entries: [{ ts: 1, jobId: "other-job", status: "ok", summary: "wrong task" }], - total: 1, - hasMore: false, - nextOffset: null, - }); + olderOverview.resolve( + createCronRunsResult([{ ts: 1, jobId: "other-job", status: "ok", summary: "wrong task" }]), + ); await expect(olderLoad).resolves.toBe("skipped"); expect(state.cronRunsJobId).toBe("selected-job"); @@ -1954,24 +1732,13 @@ describe("cron controller", () => { }); it("does not let a deferred selected job replace the current overview", async () => { - const olderJobHistory = createDeferred(); const overviewEntry = { ts: 2, jobId: "overview-job", status: "ok" as const, summary: "current overview", }; - const request = vi - .fn() - .mockImplementationOnce(() => olderJobHistory.promise) - .mockResolvedValueOnce({ - entries: [overviewEntry], - total: 1, - hasMore: false, - nextOffset: null, - }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const { older: olderJobHistory, state } = createCronRunsRace([overviewEntry], { cronRunsScope: "job", cronRunsJobId: "selected-job", }); @@ -1981,12 +1748,9 @@ describe("cron controller", () => { state.cronRunsJobId = null; await expect(loadCronRuns(state, null)).resolves.toBe("ok"); - olderJobHistory.resolve({ - entries: [{ ts: 1, jobId: "selected-job", status: "ok", summary: "stale task" }], - total: 1, - hasMore: false, - nextOffset: null, - }); + olderJobHistory.resolve( + createCronRunsResult([{ ts: 1, jobId: "selected-job", status: "ok", summary: "stale task" }]), + ); await expect(olderLoad).resolves.toBe("skipped"); expect(state.cronRunsJobId).toBeNull(); @@ -1994,24 +1758,13 @@ describe("cron controller", () => { }); it("drops an older paginated response after run-history filters are replaced", async () => { - const olderPage = createDeferred(); const currentEntry = { ts: 3, jobId: "filtered-job", status: "error" as const, summary: "filtered result", }; - const request = vi - .fn() - .mockImplementationOnce(() => olderPage.promise) - .mockResolvedValueOnce({ - entries: [currentEntry], - total: 1, - hasMore: false, - nextOffset: null, - }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const { older: olderPage, state } = createCronRunsRace([currentEntry], { cronRuns: [{ ts: 2, jobId: "previous-job", status: "ok", summary: "previous" }], cronRunsHasMore: true, cronRunsNextOffset: 1, @@ -2023,12 +1776,12 @@ describe("cron controller", () => { await expect(loadCronRuns(state, null)).resolves.toBe("ok"); expect(state.cronRunsLoadingMore).toBe(false); - olderPage.resolve({ - entries: [{ ts: 1, jobId: "stale-job", status: "ok", summary: "stale older page" }], - total: 9, - hasMore: true, - nextOffset: 2, - }); + olderPage.resolve( + createCronRunsResult( + [{ ts: 1, jobId: "stale-job", status: "ok", summary: "stale older page" }], + { total: 9, hasMore: true, nextOffset: 2 }, + ), + ); await expect(olderLoad).resolves.toBe("skipped"); expect(state.cronRuns).toEqual([currentEntry]); @@ -2038,18 +1791,8 @@ describe("cron controller", () => { }); it("ignores a stale run-history failure after the current request succeeds", async () => { - const olderFailure = createDeferred(); const currentEntry = { ts: 2, jobId: "fresh-job", status: "ok" as const, summary: "fresh" }; - const request = vi - .fn() - .mockImplementationOnce(() => olderFailure.promise) - .mockResolvedValueOnce({ - entries: [currentEntry], - total: 1, - hasMore: false, - nextOffset: null, - }); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const { older: olderFailure, state } = createCronRunsRace([currentEntry]); const olderLoad = loadCronRuns(state, null); await expect(loadCronRuns(state, null)).resolves.toBe("ok"); @@ -2066,7 +1809,7 @@ describe("cron controller", () => { .fn() .mockImplementationOnce(() => olderOverview.promise) .mockRejectedValueOnce(new Error("current cron history unavailable")); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const state = createStateWithRequest(request); const olderLoad = loadCronRuns(state, null); await expect(loadCronRuns(state, null)).resolves.toBe("error"); @@ -2090,8 +1833,7 @@ describe("cron controller", () => { ? { entries: [], total: 0, hasMore: false, nextOffset: null } : { jobs: [], total: 0, hasMore: false, nextOffset: null }, ); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronAgentId: "writer", }); @@ -2112,9 +1854,7 @@ describe("cron controller", () => { const request = vi.fn(async () => { throw new Error("cron.runs unavailable"); }); - const state = createState({ - client: { request } as unknown as CronState["client"], - }); + const state = createStateWithRequest(request); await expect(loadCronRuns(state, null)).resolves.toBe("error"); @@ -2135,8 +1875,7 @@ describe("cron controller", () => { } return {}; }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronRunsScope: "job", cronRunsJobId: "job-due", }); @@ -2160,8 +1899,7 @@ describe("cron controller", () => { } return {}; }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronRunsScope: "job", cronRunsJobId: "job-blocked", }); @@ -2175,17 +1913,11 @@ describe("cron controller", () => { ); it("reloads the skipped run recorded for an invalid persisted specification", async () => { - const request = vi.fn(async (method: string) => { - if (method === "cron.run") { - return { ok: true, ran: false, reason: "invalid-spec" }; - } - if (method === "cron.runs") { - return { entries: [], total: 0, hasMore: false, nextOffset: null }; - } - return {}; + const request = createMethodRequest({ + "cron.run": { ok: true, ran: false, reason: "invalid-spec" }, + "cron.runs": createCronRunsResult([]), }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronRunsScope: "job", cronRunsJobId: "job-invalid", }); @@ -2213,8 +1945,7 @@ describe("cron every-interval lossless round-trip", () => { function captureUpdateState(job: CronJob) { const request = createCronRequest(job.id, { existing: true }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobs: [job], }); return { request, state }; @@ -2283,8 +2014,7 @@ describe("cron every-interval lossless round-trip", () => { it("clones a sub-minute job without rounding its interval", async () => { const request = createCronRequest("job-clone"); const sourceJob = everyJob(30_000); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronJobs: [sourceJob], }); @@ -2304,7 +2034,7 @@ describe("cron every-interval lossless round-trip", () => { describe("loadCronFailingCount", () => { it("queries the unfiltered enabled+error total and stores it", async () => { const request = vi.fn(async () => ({ jobs: [], total: 4, offset: 0, limit: 1 })); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const state = createStateWithRequest(request); await loadCronFailingCount(state); expect(request).toHaveBeenCalledWith("cron.list", { @@ -2333,7 +2063,7 @@ describe("loadCronFailingCount", () => { } return {}; }); - const state = createState({ client: { request } as unknown as CronState["client"] }); + const state = createStateWithRequest(request); await toggleCronJob(state, { id: "job-1" } as never, false); expect(state.cronFailingCount).toBe(1); @@ -2343,8 +2073,7 @@ describe("loadCronFailingCount", () => { const request = vi.fn(async () => { throw new Error("nope"); }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronFailingCount: 2, }); await loadCronFailingCount(state); @@ -2360,8 +2089,7 @@ describe("loadCronScopeStats", () => { .fn() .mockResolvedValueOnce({ jobs: [], total: 7 }) .mockResolvedValueOnce({ jobs: [{ state: { nextRunAtMs: 1234 } }], total: 1 }); - const state = createState({ - client: { request } as unknown as CronState["client"], + const state = createStateWithRequest(request, { cronAgentId: "writer", });