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).
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)
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>
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>
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.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #163 (first slice).
What
Adds
ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs— a golden-file characterization net that snapshots thePlayoutItems 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)
PlaybackOrder.Chronological(72-item golden over a 2-day window).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
start(verified during scoping), so a pinnedstart(2026-01-15 06:00 UTC) is fully deterministic.Start/Finish, never the*Offsetproperties (those call.ToLocalTime()and would make goldens machine-TZ dependent).BlockPlayoutBuildermaps template times-of-day viaTimeZoneInfo.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 withAssume.That(TimeZoneInfo.Local.BaseUtcOffset == Zero): it runs underTZ=UTC(CI) and reports inconclusive (graceful skip, not fail) under any other TZ — mirroringChannelPlaylistGoldenTests'GuardVolatileEnvironment.ETV_UPDATE_PLAYOUT_GOLDENS(deliberately separate fromETV_UPDATE_GOLDENSso the nets can't cross-rewrite). Missing golden = hard fail.Verification
ErsatzTV.Core.TestsunderTZ=UTC: 540 passed, 1 skipped (pre-existing DST test), no regressions.TZ=America/New_Yorkthe block test is skipped (inconclusive), classic still passes.dotnet format whitespace --verify(no BOM).docs/testing.mdupdated (third golden net + env var + Block TZ guard).Docs
docs/testing.md— Golden-file nets section + Core.Tests row.🤖 Generated with Claude Code
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_goldensis[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):BaseUtcOffset→GetUtcOffset(Start)(instant-accurate; removes the latent fixture-date dependency —BaseUtcOffsetis 0 year-round for DST zones like Europe/London).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=UTCboth goldens pass;TZ=America/New_YorkBlock 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