(motir-core) Story E2E — a scheduled job fires on the engine and appears on the jobs dashboard, and a worker restart honours the declared catch-up
The story's Playwright E2E: the verification_recipe, automated, against a real worker process and a real browser.
The flow it drives
- Route a scheduled job to the engine mid-spec, with
routeJobsToEngine(...)fromtests/e2e/_helpers/job-routing.ts— the file-backed overridelib/jobs/engine/cutover.tsdocuments, which exists precisely because an env var is fixed at server boot and one lane has to move a job between engines without a restart. - Start a real worker with
tests/e2e/_helpers/job-worker-process.ts, astests/e2e/jobs-postgres-engine.spec.tsalready does. - Wait on the authoritative signal — the run row — never on a sleep.
- Open
/settings/workspace/jobs, filter to that job, and confirm the run appears with the expected status and itsscheduled.<id>provenance. That page is the story's verification recipe, and this is the step that proves an operator can actually see a scheduled run on the new engine. - Restart the worker across a fire and confirm what the recorded catch-up policy says should happen: for a catch-up job, the missed fire runs on restart; for a skip job, it does not. This is the second half of the recipe and it is the only assertion in the story that exercises downtime as a real event rather than as a simulated clock.
The pacing problem, named rather than discovered
Every one of the 14 shipped cadences is too slow for a spec: the fastest is one minute. Do not re-time a production job to make the test convenient — the story's own criterion is that the schedule constants are unchanged. Choose one of the two honest routes and say in the spec's header which, and why:
- register a test-only cron job on a fast expression, alongside the fixtures the suite already builds, and drive that; or
- drive one of the 14 with the scheduler's injected clock advanced, which is exactly what the injectable
nowis for.
The first proves more of the real path; the second proves the real job. Pick deliberately.
Scope boundary
Does NOT assert production behaviour, a live cadence, or a deployed ledger — the production cutover task under the epic owns those.
Records no acceptance video. This story ships no user-observable surface of its own: /settings/workspace/jobs already exists and is unchanged, so the non-UI-story exemption applies exactly as it does on MOTIR-3415.
Does NOT change the jobs dashboard, its DTOs or its service. If the page cannot show what this spec needs to assert, that is a finding to surface, not a change to make here.
Acceptance criteria
- The spec routes a job to the engine mid-run via the file override, starts a real worker, and asserts a
succeededscheduled run — waiting on the row, not on a timer. /settings/workspace/jobsfiltered to that job shows the run, and the assertion is against the rendered row rather than the database — the recipe says open the page.- A worker restart across a fire produces the outcome the declared catch-up policy specifies, asserted for at least one catch-up job and one skip job.
- No production schedule constant is modified by this card, and the spec header states which of the two pacing routes it took and why.
- The spec cleans up its routing with
clearJobRouting()so a sibling spec against the same server is unaffected — the same disciplinejobs-flow.spec.tsrelies on. - It runs in the existing E2E lane with no new service and no new global setup step.
Context refs
tests/e2e/jobs-postgres-engine.spec.ts— the closest existing spec; extend or mirror ittests/e2e/_helpers/job-routing.ts—routeJobsToEngine/clearJobRoutingtests/e2e/_helpers/job-worker-process.ts— starting and stopping a real worker from a spectests/e2e/jobs-dashboard.spec.ts— how the dashboard is already driven and filteredtests/e2e/global-setup.ts·global-teardown.ts— the lane's worker lifecyclelib/jobs/engine/cutover.ts— why the file override exists and why it is refused in productionCLAUDE.md§ E2E — waiting on authoritative signals rather than timeouts