Skip to content

Add an on-demand cadence to app custom scheduled jobs - #6391

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6375
Sep 6, 2026
Merged

Add an on-demand cadence to app custom scheduled jobs#6391
atomantic merged 1 commit into
mainfrom
claim/issue-6375

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Adds on-demand as a first-class cadence for CoS autonomous jobs (app custom scheduled tasks). The job stays enabled and manually triggerable via POST /api/cos/jobs/:id/trigger, but no timer is ever armed for it — previously the only way to get "run when I ask" was a weekly job left disabled, which also made it un-runnable.

  • resolveIntervalMs returns an explicit null no-interval sentinel instead of ending in default: return DAY. That fall-through silently rescheduled both the on-demand cadence and any typo'd cadence as a daily job.
  • All three due/next-fire implementations handle the sentinel: autonomousJobs/scheduler.js (getDueJobs, computeNextJobRun) and cosJobScheduler.js (computeNextJobFireTime — the one that actually arms the timer, which now cancels rather than scheduling). Arithmetic on null would otherwise read as "due on every sweep" / Invalid Date.
  • createCosJobSchema.interval is now the cadence enum rather than a bare z.string(), closing the silent-daily typo hole at the boundary; updateCosJobSchema inherits it via .partial(). intervalMs stays optional and is never back-filled for on-demand.
  • New pure server/lib/autonomousJobIntervals.js owns the vocabulary (INTERVAL_OPTIONS, JOB_INTERVAL_VALUES, resolveIntervalMs, isOnDemandJob) so the Zod boundary can validate against the same list without a lib → services import. services/autonomousJobs/constants.js re-exports it, so existing deep imports are unchanged. It is deliberately import-free (time units declared locally) — a fileUtils import there is a hard failure in the many route suites that mock fileUtils with an exhaustive factory.
  • UI: ScheduleFields offers the cadence and hides the time-of-day input when it is selected; the card renders Next: On demand and never shows the Due badge for such a job.
  • The legacy scripts-state.json migration now maps an on-demand script to the real cadence (keeping its own enabled flag) instead of a disabled daily job, and derives its valid-interval set from the shared vocabulary.

Existing recurring cadences, intervalMs values, next-due rendering, and job:spawned bookkeeping are unchanged. No on-disk format change, so no migration; autonomous jobs are not federated, so no schema-version bump.

Import budget

server/lib/importScoping.test.js's tree-wide ceiling was raised 89,500 → 89,900 (actual 89,442 → 89,680), documented inline. The new module is a zero-import leaf reached by 194 suites through cosValidation.js — the floor cost of sharing one constant list with the Zod boundary, with nothing to narrow or defer. The alternative is re-declaring the cadence list at the boundary, which is the drift this issue exists to close.

Test plan

  • cd server && npm test — 2,011 files pass (one pre-existing timeout flake in routes/settings.secretsStrip.test.js under full-suite load; green in isolation).
  • cd client && npm test — 869 files pass (two pre-existing flakes under load; green in isolation). npm run lint clean.
  • New/extended coverage:
    • server/lib/autonomousJobIntervals.test.js — the sentinel is neither DAY nor NaN, no fall-through for an unknown cadence, isOnDemandJob precedence (cron-mode wins), and a server↔client lockstep guard over JOB_INTERVAL_OPTIONS (verified to fail when a label is drifted on one side only).
    • server/services/autonomousJobs.test.js — an on-demand job is never due when never run and after a week elapsed; computeNextJobRun returns null; create/update store the sentinel and drop a pinned time-of-day; every cadence plus custom still round-trips.
    • server/services/cosJobScheduler.test.jsregisterSingleJobSchedule arms no timer and cancels a stale one, including for a weekdaysOnly on-demand job that would otherwise fall into the synthesized daily cron; a recurring job still arms.
    • server/lib/cosValidation.test.js — every legal cadence accepted, unknown rejected on create and update, on-demand accepted without intervalMs.
    • client/src/components/cos/JobCard.test.jsx — selecting the cadence hides the time input, Next: On demand renders with no NaN/Invalid Date, and the Due badge stays off.

Closes #6375

Custom scheduled tasks could only recur on a clock, so "run only when I ask"
had to be faked with a weekly job left disabled — which also made it
un-runnable. `on-demand` is now a first-class cadence: the job stays enabled
and manually triggerable, but no timer is ever armed for it.

`resolveIntervalMs` now returns an explicit null no-interval sentinel instead
of ending in `default: return DAY`, which silently rescheduled an unrecognized
or on-demand cadence as a daily job. All three due/next-fire implementations
handle the sentinel — `autonomousJobs/scheduler.js` (getDueJobs and
computeNextJobRun) and `cosJobScheduler.js` (computeNextJobFireTime, the one
that actually arms the timer).

The cadence vocabulary moves to the new pure `lib/autonomousJobIntervals.js`
so `createCosJobSchema` can validate against it. `interval` was a bare
`z.string()`, so a typo'd cadence reached disk and silently became daily;
it is now the cadence enum, and `updateCosJobSchema` inherits it.
@atomantic
atomantic merged commit ab72ec6 into main Sep 6, 2026
12 checks passed
@atomantic
atomantic deleted the claim/issue-6375 branch September 6, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add an on-demand cadence to app custom scheduled jobs

1 participant