Skip to content

fix(ads): a failed job says which step failed (gh-#724) - #768

Merged
genwave-radio merged 7 commits into
mainfrom
fix/ads-failed-job-kind-724
Sep 14, 2026
Merged

genwave-radio merged 7 commits into
mainfrom
fix/ads-failed-job-kind-724

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

Fixes #724 · STORY-435 (a failed job says which step failed) · PLAN T462–T466.

🐛 What was wrong

job_kind is nulled the moment a job settles, success or failure alike, so by the time job_error is readable nobody knows whether the write or the preview produced it. The wizard's Script and Hear steps both rendered the same job.error, so a failed preview showed up on the Script step too.

✅ What changed

  • T462db/47-ad-spot-job-failed-kind-migration.sh adds station.ad_spot.job_failed_kind text with CHECK (IS NULL OR IN ('write','preview')); db/06 carries the identical column for fresh installs (gh-db/06 fresh-init mirror is missing station.schedule_special (db/36) #618 lesson), pinned by a text-equality spec. No new table, so setup.sh's migration marker stays at db/46 and its range message reports "db/47 adds no new table" — the documented degrade, not a bug.
  • T463AdSpot.JobFailedKind; StampJobAsync nulls it, ClearJobAsync sets it to the failing job's kind on failure and null on a clean finish (one SQL statement, reads the old job_kind). Fakes mirror the semantics.
  • T464AdSpotJobDto.FailedKind on the wire as failedKind; job is null only when none of job_kind, job_error, job_failed_kind is set.
  • T465JobRunner takes the kind it owns (ScriptStepwrite, HearSteppreview) and renders the error only when job.failedKind matches. The wizard-level alert keeps request failures only. A legacy row with an error but no failedKind shows on neither step; the next stamped job clears it.
  • Specs: tests/GenWave.Host.Tests/Specs/Story435_FailedJobKind.cs (11 facts, endpoint driven against an ephemeral Postgres) and admin-ui/__specs__/wizard-failed-step.spec.tsx. Red at 2b20a02, green now.

🔌 Wire evidence (T466, dev stack via BUILD=1 ./launch.sh, Kokoro stopped)

New spot for Wire Diner through the wizard, "Render preview" on the Hear step:

GET /api/ads/23 -> 200
{"id":23,"state":"draft","job":{"kind":null,"failedKind":"preview","startedAt":null,"waitingForStation":false,
 "error":"preview: ANNOUNCER line 1 of 1 failed to render (HttpRequestException)"}}

select id, state, job_kind, job_failed_kind, left(job_error,90) from station.ad_spot where job_failed_kind is not null;
 23 | draft |  | preview | preview: ANNOUNCER line 1 of 1 failed to render (HttpRequestException)

Hear step (accessibility snapshot):

- button "Render preview"
- alert: "preview: ANNOUNCER line 1 of 1 failed to render (HttpRequestException)"

Back to the Script step: the script textarea and voice cast render, no alert node. Screenshots of both steps are on the dev box (t466-hear-error.png, t466-script-clean.png) and can be attached on request.

📝 Notes for review

  • Existing rows that failed before this migration keep job_error with a null job_failed_kind; the migration does not backfill, so their stale error shows on neither step until the next job runs. Accepted rather than guessed.
  • db/47 must land in the v5.8.2 pins PR's upgrade note.

Do not merge without Dean.

STORY-435: a failed write or preview job must read back with the kind
that failed (job_failed_kind / failedKind) so the wizard can route the
error to the Script or Hear step. Host facts fail on the missing column
and DTO field; the Jest spec fails on both steps showing the other's
error.
Adds the job_failed_kind column (text, CHECK write|preview, nullable) to
station.ad_spot via an idempotent ADD COLUMN IF NOT EXISTS, mirrored in
db/06 for fresh installs. A text-equality spec pins the CHECK predicate
identical across db/47 and db/06. Story346's marker comment is updated:
db/47 creates no table, so setup.sh keeps db/46 as its marker and reports
the db/47 gap honestly.

Why: job_kind is nulled when a job settles, so the failed step is lost by
the time job_error is readable (gh-#724, STORY-435).
AdSpot gains JobFailedKind; AdSpotRepository maps job_failed_kind on every
read, nulls it when a fresh job is stamped, and on ClearJobAsync copies the
old job_kind into it only when an error is recorded. Ads test fakes mirror
the same semantics.

Why: job_kind is nulled when a job settles, so without its own column the
failed step is lost by the time job_error is readable (gh-#724, STORY-435).
AdSpotJobDto gains FailedKind after Kind; ToJobDto returns a job object
whenever any of job_kind, job_error or job_failed_kind is set. The
admin-ui AdSpotJobDto type gains failedKind and every spec literal
carries it.

Why: the wizard needs to know which step's job failed so it can show the
error on that step alone (gh-#724, STORY-435).
JobRunner takes the job kind it owns ("write" for ScriptStep, "preview"
for HearStep) and renders job.error only when job.failedKind matches.
A failed preview therefore shows on the Hear step alone and a failed
write on the Script step alone, instead of both steps echoing the same
message (STORY-435 AC6–AC8, gh-#724). The wizard-level alert still
carries request failures only. A legacy row with an error but no
failedKind shows on neither step; the next stamped job clears it.
PR #764 added renderWithinMinutes to AdSpotDto after this branch's
wizard-failed-step spec was written, so CI's typecheck:specs failed on
the merged tree. The fixture now sets it to null like the other specs.
@genwave-radio
genwave-radio merged commit e8c1270 into main Sep 14, 2026
11 checks passed
@genwave-radio
genwave-radio deleted the fix/ads-failed-job-kind-724 branch September 14, 2026 02:23
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ads API: a failed write/preview job loses its kind — the wizard cannot tell which step failed

1 participant