test(163): golden characterization tests for playout building (Classic + Block) #382

Merged
timothy merged 2 commits from feat/163-playout-goldens into main 2026-07-17 00:01:34 +02:00
Owner

Closes #163 (first slice).

What

Adds ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs — a golden-file characterization net that snapshots the PlayoutItems each playout builder produces over a pinned build window. The scheduling counterpart to the M3U (#11) and XMLTV (#28) goldens, and the regression net that de-risks the scheduling-refactor chain #380 → #70/#71/#176.

Coverage (this slice)

  • Classic builder — PlaybackOrder.Chronological (72-item golden over a 2-day window).
  • Block builder — single 60-min block/day, chronological cursor carried across days.

Sequential (YAML) + Scripted goldens are tracked as a follow-up in #381 — they need a YAML fixture / an external-process (Cli.Wrap + localhost HTTP) harness respectively, not just a clock seam.

How it stays deterministic

  • The builders read no wall clock — time enters only via the caller-supplied start (verified during scoping), so a pinned start (2026-01-15 06:00 UTC) is fully deterministic.
  • Snapshots the raw UTC Start/Finish, never the *Offset properties (those call .ToLocalTime() and would make goldens machine-TZ dependent).
  • Block TZ guard: BlockPlayoutBuilder maps template times-of-day via TimeZoneInfo.Local, so its output is intrinsically machine-TZ dependent. As a characterization test (no production change — a real TZ seam is #380's job), it's guarded with Assume.That(TimeZoneInfo.Local.BaseUtcOffset == Zero): it runs under TZ=UTC (CI) and reports inconclusive (graceful skip, not fail) under any other TZ — mirroring ChannelPlaylistGoldenTests' GuardVolatileEnvironment.
  • Regen via ETV_UPDATE_PLAYOUT_GOLDENS (deliberately separate from ETV_UPDATE_GOLDENS so the nets can't cross-rewrite). Missing golden = hard fail.

Verification

  • Full ErsatzTV.Core.Tests under TZ=UTC: 540 passed, 1 skipped (pre-existing DST test), no regressions.
  • Both goldens proven non-vacuous: perturbing an input (PlaybackOrder→Random for classic; a duration for block) flips the golden.
  • Block guard confirmed: under TZ=America/New_York the block test is skipped (inconclusive), classic still passes.
  • Formatting: touched file clean under dotnet format whitespace --verify (no BOM).
  • docs/testing.md updated (third golden net + env var + Block TZ guard).

Docs

  • docs/testing.md — Golden-file nets section + Core.Tests row.

🤖 Generated with Claude Code

Closes #163 (first slice). ## What Adds `ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs` — a golden-file characterization net that snapshots the `PlayoutItem`s each playout builder produces over a pinned build window. The scheduling counterpart to the M3U (#11) and XMLTV (#28) goldens, and **the regression net that de-risks the scheduling-refactor chain** #380 → #70/#71/#176. ## Coverage (this slice) - **Classic** builder — `PlaybackOrder.Chronological` (72-item golden over a 2-day window). - **Block** builder — single 60-min block/day, chronological cursor carried across days. **Sequential (YAML)** + **Scripted** goldens are tracked as a follow-up in **#381** — they need a YAML fixture / an external-process (`Cli.Wrap` + localhost HTTP) harness respectively, not just a clock seam. ## How it stays deterministic - The builders read **no wall clock** — time enters only via the caller-supplied `start` (verified during scoping), so a pinned `start` (`2026-01-15 06:00 UTC`) is fully deterministic. - Snapshots the **raw UTC `Start`/`Finish`**, never the `*Offset` properties (those call `.ToLocalTime()` and would make goldens machine-TZ dependent). - **Block TZ guard:** `BlockPlayoutBuilder` maps template times-of-day via `TimeZoneInfo.Local`, so its output is intrinsically machine-TZ dependent. As a *characterization* test (no production change — a real TZ seam is #380's job), it's guarded with `Assume.That(TimeZoneInfo.Local.BaseUtcOffset == Zero)`: it **runs under `TZ=UTC` (CI)** and reports **inconclusive** (graceful skip, not fail) under any other TZ — mirroring `ChannelPlaylistGoldenTests`' `GuardVolatileEnvironment`. - Regen via **`ETV_UPDATE_PLAYOUT_GOLDENS`** (deliberately separate from `ETV_UPDATE_GOLDENS` so the nets can't cross-rewrite). Missing golden = hard fail. ## Verification - Full `ErsatzTV.Core.Tests` under `TZ=UTC`: **540 passed, 1 skipped** (pre-existing DST test), no regressions. - Both goldens proven **non-vacuous**: perturbing an input (PlaybackOrder→Random for classic; a duration for block) flips the golden. - Block guard confirmed: under `TZ=America/New_York` the block test is **skipped** (inconclusive), classic still passes. - Formatting: touched file clean under `dotnet format whitespace --verify` (no BOM). - `docs/testing.md` updated (third golden net + env var + Block TZ guard). ## Docs - `docs/testing.md` — Golden-file nets section + Core.Tests row. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-16 23:42:02 +02:00
test(163): golden characterization tests for playout building (Classic + Block)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 44s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m33s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
dc5d889ccb
Add ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs — a
golden-file net that snapshots the PlayoutItems each builder produces over a
pinned build window; the scheduling counterpart to the M3U (#11) / XMLTV (#28)
goldens. This is the regression net that de-risks the scheduling-refactor
chain (#380 -> #70/#71/#176).

Coverage this slice:
- Classic (PlaybackOrder.Chronological)
- Block (TimeZoneInfo.Local-guarded to UTC via Assume: runs in CI, skips
  gracefully under a non-UTC TZ; a real TZ seam is #380's scope)

Determinism: the builders read no wall clock (time enters only via the
caller-supplied start), so a pinned start is fully deterministic. Snapshots the
raw UTC Start/Finish, not the *Offset properties (which .ToLocalTime()). Regen
via ETV_UPDATE_PLAYOUT_GOLDENS (deliberately separate from ETV_UPDATE_GOLDENS).
Both goldens proven non-vacuous.

Sequential (YAML) + Scripted goldens tracked as a follow-up in #381.
Docs: docs/testing.md updated (third golden net + env var + Block TZ guard).

fixes #163

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
timothy added 1 commit 2026-07-16 23:47:57 +02:00
test(163): apply review nits — instant-accurate Block TZ guard + determinism notes
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m9s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 40s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 7m33s
1131ecacfb
Follow-up to the cold adversarial review of PR #382 (MERGEABLE-WITH-NITS):
- Block TZ guard: BaseUtcOffset -> GetUtcOffset(Start). BaseUtcOffset is zero
  year-round for DST zones like Europe/London, so it would pass in a summer-dated
  fixture where London != UTC; GetUtcOffset pins the actual build instant and is
  correct regardless of fixture date. (Safe today — mid-Jan fixture — but removes
  the latent fixture-date dependency the reviewer flagged.)
- Document the determinism invariants the fixture relies on: golden captures raw
  builder output (pre-trim AddedItems), Classic is TZ-independent for the captured
  fields (no guard needed), and ResetPlayout's random Seed can't perturb the
  Chronological fixture (RandomStartPoint/ShuffleScheduleItems false, distinct
  release dates).

Re-verified: TZ=UTC both goldens pass; TZ=America/New_York Block skips, Classic
passes; no golden drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Cold-context adversarial review (independent, review-only) of the diff: MERGEABLE-WITH-NITS, no blocking findings. The reviewer traced both goldens against the actual builders and confirmed correct, continuous, deterministic-under-UTC output; real string-equality comparison; hard-fail on missing golden; Regenerate_goldens is [Explicit] so it can't rewrite during a normal run.

Applied the two worthwhile nits in follow-up commit 1131ecac (exactly as the reviewer prescribed):

  • Block TZ guard BaseUtcOffset → GetUtcOffset(Start) (instant-accurate; removes the latent fixture-date dependency — BaseUtcOffset is 0 year-round for DST zones like Europe/London).
  • Documented the determinism invariants (golden captures raw pre-trim builder output; Classic is TZ-independent for the captured fields hence unguarded; ResetPlayout's random Seed can't perturb the Chronological fixture).

Remaining observations (isolation relies on NUnit's sequential default, mirroring the sibling ChannelGuideGoldenTests) are non-blocking and unchanged.

Re-verified after the follow-up: TZ=UTC both goldens pass; TZ=America/New_York Block skips (inconclusive) + Classic passes; no golden drift; clean build.

The delta from the reviewed diff (dc5d889c) to current head is exactly the reviewer's own prescribed nits + comments, empirically re-verified under both TZs.

Review-verdict: MERGEABLE @ 1131ecacfb

Cold-context adversarial review (independent, review-only) of the diff: **MERGEABLE-WITH-NITS**, no blocking findings. The reviewer traced both goldens against the actual builders and confirmed correct, continuous, deterministic-under-UTC output; real string-equality comparison; hard-fail on missing golden; `Regenerate_goldens` is `[Explicit]` so it can't rewrite during a normal run. Applied the two worthwhile nits in follow-up commit `1131ecac` (exactly as the reviewer prescribed): - Block TZ guard `BaseUtcOffset` → `GetUtcOffset(Start)` (instant-accurate; removes the latent fixture-date dependency — `BaseUtcOffset` is 0 year-round for DST zones like Europe/London). - Documented the determinism invariants (golden captures raw pre-trim builder output; Classic is TZ-independent for the captured fields hence unguarded; ResetPlayout's random Seed can't perturb the Chronological fixture). Remaining observations (isolation relies on NUnit's sequential default, mirroring the sibling `ChannelGuideGoldenTests`) are non-blocking and unchanged. Re-verified after the follow-up: `TZ=UTC` both goldens pass; `TZ=America/New_York` Block skips (inconclusive) + Classic passes; no golden drift; clean build. The delta from the reviewed diff (`dc5d889c`) to current head is exactly the reviewer's own prescribed nits + comments, empirically re-verified under both TZs. Review-verdict: MERGEABLE @ 1131ecacfb09ff7a30b12e2fe0d8df7b00e6c326
timothy merged commit 181eabdded into main 2026-07-17 00:01:34 +02:00
timothy deleted branch feat/163-playout-goldens 2026-07-17 00:01:34 +02:00
Sign in to join this conversation.