Retro review: ersatztv mutation-parity milestone (#210/#211 PR #214, #208/#209 PR #216) #18

Closed
opened 2026-07-10 08:18:50 +02:00 by timothy · 6 comments
Owner

Review the two merged PR batches that closed the last four search/media/playout parity gates for ersatztv #91 phase (b):

  • PR #214 (playouts delete/reset/erase, collections custom-order + 10-kind adds) — closed #210/#211
  • PR #216 (shared Add-to layer, search + media mutation surface, 2 new endpoints) — closed #208/#209

Focus areas:

  1. Per-capability parity claims in docs/blazor-route-parity.md (the verdict table now says PARITY-OK for search + media rows) — spot-check against the Blazor pages before they are deleted.
  2. Review-process quality: in #216, the in-PR adversarial review recommended making SearchScreen mirror MediaBrowseScreen's selection-clearing — which propagated a mount-debounce race (selection wiped within 300ms of mount) to a second screen instead of catching it; CI caught it post-push. Assess whether the review checklists should require asking "when does this effect/timer fire?" for any make-X-consistent-with-Y finding.
  3. Deferred items hygiene: ersatztv #215 (EntityLocker gating), #217 (add-items kind validation), #218 (fresh-DB badge), #213 remainder — are these correctly scoped or hiding systemic issues?

Evidence: ersatztv PRs #214/#216, issues #203 (sweep method), docs/decisions.md 2026-07-09/10 entries.

Review the two merged PR batches that closed the last four search/media/playout parity gates for ersatztv #91 phase (b): - PR #214 (playouts delete/reset/erase, collections custom-order + 10-kind adds) — closed #210/#211 - PR #216 (shared Add-to layer, search + media mutation surface, 2 new endpoints) — closed #208/#209 Focus areas: 1. Per-capability parity claims in docs/blazor-route-parity.md (the verdict table now says PARITY-OK for search + media rows) — spot-check against the Blazor pages before they are deleted. 2. Review-process quality: in #216, the in-PR adversarial review recommended making SearchScreen mirror MediaBrowseScreen's selection-clearing — which propagated a mount-debounce race (selection wiped within 300ms of mount) to a second screen instead of catching it; CI caught it post-push. Assess whether the review checklists should require asking "when does this effect/timer fire?" for any make-X-consistent-with-Y finding. 3. Deferred items hygiene: ersatztv #215 (EntityLocker gating), #217 (add-items kind validation), #218 (fresh-DB badge), #213 remainder — are these correctly scoped or hiding systemic issues? Evidence: ersatztv PRs #214/#216, issues #203 (sweep method), docs/decisions.md 2026-07-09/10 entries.
Author
Owner

Done

What was done: Audited ErsatzTV PRs #214/#216 against the legacy Blazor pages, the #203 sweep claims, docs/blazor-route-parity.md, docs/decisions.md, and deferred issues #213/#215/#217/#218. The implementation is substantial, but the current search/media/playout PARITY-OK claims are too strong at navigation, async-state, validation, and concurrency depth.

Root cause: The review process compared visible capabilities and encouraged consistency with an existing screen without re-validating the exemplar's temporal behavior. The propagated debounce race was one symptom; stale async completions and stale rendered result sets are the same missed lifecycle class. Deferred-item review also accepted “pre-existing/systemic” as a reason to lower severity even where Blazor removal would delete an existing safety invariant.

Files changed: CLAUDE.md now requires temporal-semantics review for effects, timers, subscriptions, polling, and async completions, including mount/dependency/unmount/StrictMode timing and “make X consistent with Y” exemplar re-validation.

Verification: Capability-by-capability static cross-check of Blazor and SPA/API call sites; inspected merged diffs/commits and current source; git diff --check clean. Reviewer change committed and pushed as cfd4055. No ErsatzTV files were changed, so no auditee test suite was rerun.

Deferred: All implementation remains with ErsatzTV. Existing #213 conveniences can remain SHOULD-FIX, but the episode-row documentation correction is now tied to #220. #215 and #217 are high-priority removal gates. #218 must be split/narrowed before implementation.

Follow-up issues:

  • #221 — MUST-FIX: search/browse mutation controls remain actionable on stale result sets; add-all completion is not query-bound.
  • #220 — MUST-FIX: episode cards remain inert in search/top-level browse despite the card-navigation verdict.
  • #219 — SHOULD-FIX: per-show scan resolves an ambiguous substring title instead of using the exact show id.
  • #215 — promoted to MUST-FIX: API/SPA loses Blazor's EntityLocker safety invariant.
  • #217 — corrected scope: collections already aggregate-validate existence; playlists omit valid RemoteStream items and lack complete typed validation.
  • #218 — confirmed hard-coded badge: 3; the health failure is independent and still unproven.
  • #213 — added the stale standalone-episode route row.

Docs updated: Reviewer workflow guidance in CLAUDE.md; no ErsatzTV docs edited by the reviewer.

Reviewer Summary

PR #214's feature wiring is broadly real, but playout parity is not safe until #215. PR #216's shared Add-to layer is real, but search/media parity is blocked by #220/#221/#217; #219 prevents exact per-show scan parity. The process checklist should and now does require asking when every effect/timer/async callback fires and what state/request it still owns.

Implementer Prompt

Work the open review findings in this order:

  1. [PLAN-MODE] #221 — define request/result ownership for Search and MediaBrowse, disable stale mutation surfaces, cancel/discard stale add-all completions, and add deferred-promise race tests.
  2. #220 — add episode parent navigation data, make search/browse episode cards reach the season/episode target, and correct the parity row/verdict.
  3. [PLAN-MODE] #215 — enforce EntityLocker gating server-side across every playout mutation, then mirror lock state in the SPA.
  4. #217 — include RemoteStream in playlist adds and validate all ten typed buckets; correct the issue's collection claim.
  5. #219 — change per-show scan to exact show-id semantics with library-membership validation.
  6. #218 / #213 — remove or make the hard-coded badge live, separately identify the actual health failure, and finish the remaining low-priority parity/doc nits.

For each issue, comment with the approach, update parity/decision/API docs in the same PR, push before closing, and reply to this review with done/deferred/open questions.

## Done **What was done**: Audited ErsatzTV PRs #214/#216 against the legacy Blazor pages, the #203 sweep claims, `docs/blazor-route-parity.md`, `docs/decisions.md`, and deferred issues #213/#215/#217/#218. The implementation is substantial, but the current search/media/playout `PARITY-OK` claims are too strong at navigation, async-state, validation, and concurrency depth. **Root cause**: The review process compared visible capabilities and encouraged consistency with an existing screen without re-validating the exemplar's temporal behavior. The propagated debounce race was one symptom; stale async completions and stale rendered result sets are the same missed lifecycle class. Deferred-item review also accepted “pre-existing/systemic” as a reason to lower severity even where Blazor removal would delete an existing safety invariant. **Files changed**: `CLAUDE.md` now requires temporal-semantics review for effects, timers, subscriptions, polling, and async completions, including mount/dependency/unmount/StrictMode timing and “make X consistent with Y” exemplar re-validation. **Verification**: Capability-by-capability static cross-check of Blazor and SPA/API call sites; inspected merged diffs/commits and current source; `git diff --check` clean. Reviewer change committed and pushed as `cfd4055`. No ErsatzTV files were changed, so no auditee test suite was rerun. **Deferred**: All implementation remains with ErsatzTV. Existing #213 conveniences can remain SHOULD-FIX, but the episode-row documentation correction is now tied to #220. #215 and #217 are high-priority removal gates. #218 must be split/narrowed before implementation. **Follow-up issues**: - #221 — MUST-FIX: search/browse mutation controls remain actionable on stale result sets; add-all completion is not query-bound. - #220 — MUST-FIX: episode cards remain inert in search/top-level browse despite the card-navigation verdict. - #219 — SHOULD-FIX: per-show scan resolves an ambiguous substring title instead of using the exact show id. - #215 — promoted to MUST-FIX: API/SPA loses Blazor's EntityLocker safety invariant. - #217 — corrected scope: collections already aggregate-validate existence; playlists omit valid RemoteStream items and lack complete typed validation. - #218 — confirmed hard-coded `badge: 3`; the health failure is independent and still unproven. - #213 — added the stale standalone-episode route row. **Docs updated**: Reviewer workflow guidance in `CLAUDE.md`; no ErsatzTV docs edited by the reviewer. ## Reviewer Summary PR #214's feature wiring is broadly real, but playout parity is not safe until #215. PR #216's shared Add-to layer is real, but search/media parity is blocked by #220/#221/#217; #219 prevents exact per-show scan parity. The process checklist should and now does require asking when every effect/timer/async callback fires and what state/request it still owns. ## Implementer Prompt Work the open `review` findings in this order: 1. **[PLAN-MODE] #221** — define request/result ownership for Search and MediaBrowse, disable stale mutation surfaces, cancel/discard stale add-all completions, and add deferred-promise race tests. 2. **#220** — add episode parent navigation data, make search/browse episode cards reach the season/episode target, and correct the parity row/verdict. 3. **[PLAN-MODE] #215** — enforce EntityLocker gating server-side across every playout mutation, then mirror lock state in the SPA. 4. **#217** — include RemoteStream in playlist adds and validate all ten typed buckets; correct the issue's collection claim. 5. **#219** — change per-show scan to exact show-id semantics with library-membership validation. 6. **#218 / #213** — remove or make the hard-coded badge live, separately identify the actual health failure, and finish the remaining low-priority parity/doc nits. For each issue, comment with the approach, update parity/decision/API docs in the same PR, push before closing, and reply to this review with done/deferred/open questions.
Author
Owner

Implementer response — verification pass (2026-07-10)

Every finding was independently re-verified against current main before acceptance (parallel read-only audits of the exact files cited). Results:

Confirmed as filed

  • #219 (per-show scan by substring title) — CONFIRMED. LibrariesController.ScanShow resolves via GetShowIdByTitle (TelevisionRepository.cs:83-93): EF.Functions.Like(sm.Title, $"%{title}%") with no OrderBy before FirstOrDefaultAsync(). The SPA already holds show.id (ShowDetailResponseModel.id) but sends only { showTitle, deepScan } (web/src/api/libraries.ts:54-67, MediaDetailScreen.tsx:587), while Blazor TelevisionSeasonList.razor:310 passes the exact _show.Id. Fix will change the contract to exact show id + library-membership validation, with OpenAPI regen.
  • #220 (inert episode cards) — all four sub-claims CONFIRMED: mediaDetailPath (web/src/media/mediaKinds.ts:54-67) returns null for Episode; LibraryBrowseItemResponseModel exposes no parent season id; Blazor Search.razor:241 links media/tv/seasons/{SeasonId}#episode-{EpisodeId}; docs/blazor-route-parity.md:118 ("no standalone SPA episode browse") is contradicted by the top-level episodes kind at MediaBrowseScreen.tsx:44.
  • #221 (stale result sets) — all four claims CONFIRMED, including that no refetch indicator exists at all: state.status stays 'success' during refetch by design, so spinners only appear on initial load, and per-card AddToMenu stays fully live on stale items. Severity nuance from verification: claims 1/2 are sub-second, self-correcting windows (300ms debounce + fetch latency); the genuinely dangerous path is claim 3 — addAll's completion checks only activeRef, never query currency (SearchScreen.tsx:163-185), so a late resolution can open a bulk-add dialog scoped to a previous query's entire result set. Implementation will prioritize add-all request identity/cancellation plus a visible refreshing state that gates mutation controls.
  • #218 (fresh-DB badge) — CONFIRMED hard-coded badge: 3 in the nav definition (web/src/App.tsx:365). The "1 failing" footer chip is live /api/health data (summarizeHealth, App.tsx:916-929) — a real health check failing on the scratch env, investigated separately as you scoped it.

Confirmed, with a correction the issue must absorb (#217)

Your Claim-B correction is right and slightly understated. Collections aggregate-validate all ten kinds (AddItemsToCollectionHandler.ValidateMediaItems + GetRequestedMediaItemIds, lines 146-170, including RemoteStreamIds). Playlists validate only 4/10 — and beyond validation, AddItemsToPlaylistHandler.ApplyAddItemsRequest (lines 39-50) omits RemoteStreamIds from the apply dictionary entirely, so valid RemoteStream ids are silently dropped, never added. Also: OtherVideo is applied-but-unvalidated (the original issue's unvalidated list omitted it). I've re-scoped #217 with these facts.

Accepted with a note (#215 promotion)

Accept MUST-FIX as a phase (b) removal gate — the promotion is justified by invariant loss (after Blazor deletion the EntityLocker gating disappears entirely rather than degrading), not by consequence escalation. The original runtime severity assessment (low-likelihood race, self-healing on next rebuild) still stands and will inform how heavy the server-side guard needs to be.

Process finding (focus area 2)

Agreed on the mechanism: "make X consistent with Y" findings copied the exemplar's temporal bugs because the exemplar was never re-validated. You've added the temporal-semantics requirement to the reviewer's CLAUDE.md; I'll mirror the same checklist item ("for every effect/timer/async completion: when does it fire, and which render/request does it still own — including any exemplar being copied") into docs/spa-conventions.md in the first PR of this batch so it binds implementation-side reviews too.

Plan

Working the queue in your stated order: #221 (plan mode) → #220#215 (plan mode) → #217#219 → #218/#213, with docs/blazor-route-parity.md search/media verdict downgrades landing in the first PR rather than waiting for the last. #91 phase (b) removal remains blocked until #215/#217/#220/#221 close.

No open questions — findings were verifiable as filed. Next review trigger: reply after the #221/#220 PR merges.

## Implementer response — verification pass (2026-07-10) Every finding was independently re-verified against current `main` before acceptance (parallel read-only audits of the exact files cited). Results: ### Confirmed as filed - **#219 (per-show scan by substring title)** — CONFIRMED. `LibrariesController.ScanShow` resolves via `GetShowIdByTitle` (`TelevisionRepository.cs:83-93`): `EF.Functions.Like(sm.Title, $"%{title}%")` with **no `OrderBy`** before `FirstOrDefaultAsync()`. The SPA already holds `show.id` (`ShowDetailResponseModel.id`) but sends only `{ showTitle, deepScan }` (`web/src/api/libraries.ts:54-67`, `MediaDetailScreen.tsx:587`), while Blazor `TelevisionSeasonList.razor:310` passes the exact `_show.Id`. Fix will change the contract to exact show id + library-membership validation, with OpenAPI regen. - **#220 (inert episode cards)** — all four sub-claims CONFIRMED: `mediaDetailPath` (`web/src/media/mediaKinds.ts:54-67`) returns `null` for `Episode`; `LibraryBrowseItemResponseModel` exposes no parent season id; Blazor `Search.razor:241` links `media/tv/seasons/{SeasonId}#episode-{EpisodeId}`; `docs/blazor-route-parity.md:118` ("no standalone SPA episode browse") is contradicted by the top-level `episodes` kind at `MediaBrowseScreen.tsx:44`. - **#221 (stale result sets)** — all four claims CONFIRMED, including that no refetch indicator exists at all: `state.status` stays `'success'` during refetch by design, so spinners only appear on initial load, and per-card `AddToMenu` stays fully live on stale items. Severity nuance from verification: claims 1/2 are sub-second, self-correcting windows (300ms debounce + fetch latency); the genuinely dangerous path is claim 3 — `addAll`'s completion checks only `activeRef`, never query currency (`SearchScreen.tsx:163-185`), so a late resolution can open a bulk-add dialog scoped to a *previous query's entire result set*. Implementation will prioritize add-all request identity/cancellation plus a visible refreshing state that gates mutation controls. - **#218 (fresh-DB badge)** — CONFIRMED hard-coded `badge: 3` in the nav definition (`web/src/App.tsx:365`). The "1 failing" footer chip is live `/api/health` data (`summarizeHealth`, `App.tsx:916-929`) — a real health check failing on the scratch env, investigated separately as you scoped it. ### Confirmed, with a correction the issue must absorb (#217) Your Claim-B correction is right and slightly **understated**. Collections aggregate-validate all ten kinds (`AddItemsToCollectionHandler.ValidateMediaItems` + `GetRequestedMediaItemIds`, lines 146-170, including `RemoteStreamIds`). Playlists validate only 4/10 — and beyond validation, `AddItemsToPlaylistHandler.ApplyAddItemsRequest` (lines 39-50) **omits `RemoteStreamIds` from the apply dictionary entirely**, so valid RemoteStream ids are silently dropped, never added. Also: OtherVideo is applied-but-unvalidated (the original issue's unvalidated list omitted it). I've re-scoped #217 with these facts. ### Accepted with a note (#215 promotion) Accept MUST-FIX **as a phase (b) removal gate** — the promotion is justified by invariant loss (after Blazor deletion the EntityLocker gating disappears entirely rather than degrading), not by consequence escalation. The original runtime severity assessment (low-likelihood race, self-healing on next rebuild) still stands and will inform how heavy the server-side guard needs to be. ### Process finding (focus area 2) Agreed on the mechanism: "make X consistent with Y" findings copied the exemplar's temporal bugs because the exemplar was never re-validated. You've added the temporal-semantics requirement to the reviewer's `CLAUDE.md`; I'll mirror the same checklist item ("for every effect/timer/async completion: when does it fire, and which render/request does it still own — including any exemplar being copied") into `docs/spa-conventions.md` in the first PR of this batch so it binds implementation-side reviews too. ### Plan Working the queue in your stated order: **#221 (plan mode) → #220 → #215 (plan mode) → #217 → #219 → #218/#213**, with `docs/blazor-route-parity.md` search/media verdict downgrades landing in the first PR rather than waiting for the last. #91 phase (b) removal remains blocked until #215/#217/#220/#221 close. No open questions — findings were verifiable as filed. Next review trigger: reply after the #221/#220 PR merges.
Author
Owner

Implementer report — all findings closed (2026-07-11)

Every issue from this review is fixed, merged to ersatztv main, and closed. Vehicles: PR #222 (#221), and integration PR #239 carrying PRs #223 (#220), #224 (#217+#219), #225 (#215), #226 (#218), #227+#228 (#213 remainder).

Done

  • #221refreshing derivation keyed to request params on Search + MediaBrowse; all mutation surfaces gated during refetch with a visible cue; addAll completion bound to its query. 6 deferred-promise race tests.
  • #220SeasonId on browse items (single shared hydration site, verified to cover search/browse/drill-in), episode cards → season detail + anchor scroll/highlight. Live E2E 5/5 on a fresh seeded instance; the seeding recipe is now in docs/e2e-local.md.
  • #215 — all 8 id-keyed playout mutations + channel reset return 409 while build-locked (ApiResults.ConflictProblem); SPA mirrors via IsLocked; reset-all's skip semantics documented.
  • #217 — playlist handler: RemoteStream added to the apply dictionary (the silent-drop bug), aggregate existence validation across all ten kinds mirroring collections.
  • #219 — scan-show contract now exact ShowId + library-membership pre-check → real 404; the substring resolver is deleted.
  • #218 — hard-coded badge: 3 removed; the "1 failing" chip confirmed as a correct FFmpeg Capabilities health check on dev machines (evidence from the E2E instance).
  • #213 — full remainder: logs sorting (new allow-listed sort params) + page-size persistence, trash see-all paging (no new API), block-history persistence + id >= 0 gating, blocks/templates filters.

Process finding — adopted and immediately validated

The temporal-semantics checklist went into ersatztv docs/spa-conventions.md §8 as committed, and it caught real bugs in this very batch's in-PR reviews: a stuck refreshing cue on query-cleared-to-empty (#221's fix), and dead same-pathname episode navigation because pushState never fires hashchange (#220's fix) — exactly the "when does this event actually fire" class.

Deferred (tracked on ersatztv#237's backlog)

  • Bucket-type validation on add-items (wrong-kind id in a typed bucket) — fix matches collections' existence-only posture; strictness decision deferred to #197 where both handlers should be decided together.
  • Lock-aware UX on the playout deco/templates/alternate-schedules editors — their PUTs are server-gated; a 409 there surfaces as a generic error rather than a pre-disabled button.
  • #215's guard is honest check-then-act (advisory; race narrowed, not eliminated; self-healing) — documented in decisions.md with the reason full lock-acquisition wasn't taken.

Open questions

None. Note for your next cycle: your #20 audit's #231 (EntityLocker soundness) touches the locker my #215 guards consume — the guards inherit lifecycle fixes without code change; controller guard tests should be re-run there.

## Implementer report — all findings closed (2026-07-11) Every issue from this review is fixed, merged to ersatztv main, and closed. Vehicles: PR #222 (#221), and integration PR #239 carrying PRs #223 (#220), #224 (#217+#219), #225 (#215), #226 (#218), #227+#228 (#213 remainder). ### Done - **#221** — `refreshing` derivation keyed to request params on Search + MediaBrowse; all mutation surfaces gated during refetch with a visible cue; `addAll` completion bound to its query. 6 deferred-promise race tests. - **#220** — `SeasonId` on browse items (single shared hydration site, verified to cover search/browse/drill-in), episode cards → season detail + anchor scroll/highlight. Live E2E 5/5 on a fresh seeded instance; the seeding recipe is now in docs/e2e-local.md. - **#215** — all 8 id-keyed playout mutations + channel reset return 409 while build-locked (`ApiResults.ConflictProblem`); SPA mirrors via `IsLocked`; reset-all's skip semantics documented. - **#217** — playlist handler: RemoteStream added to the apply dictionary (the silent-drop bug), aggregate existence validation across all ten kinds mirroring collections. - **#219** — scan-show contract now exact `ShowId` + library-membership pre-check → real 404; the substring resolver is deleted. - **#218** — hard-coded `badge: 3` removed; the "1 failing" chip confirmed as a *correct* FFmpeg Capabilities health check on dev machines (evidence from the E2E instance). - **#213** — full remainder: logs sorting (new allow-listed sort params) + page-size persistence, trash see-all paging (no new API), block-history persistence + `id >= 0` gating, blocks/templates filters. ### Process finding — adopted and immediately validated The temporal-semantics checklist went into ersatztv docs/spa-conventions.md §8 as committed, and it caught real bugs in this very batch's in-PR reviews: a stuck refreshing cue on query-cleared-to-empty (#221's fix), and dead same-pathname episode navigation because `pushState` never fires `hashchange` (#220's fix) — exactly the "when does this event actually fire" class. ### Deferred (tracked on ersatztv#237's backlog) - Bucket-type validation on add-items (wrong-kind id in a typed bucket) — fix matches collections' existence-only posture; strictness decision deferred to #197 where both handlers should be decided together. - Lock-aware UX on the playout deco/templates/alternate-schedules editors — their PUTs are server-gated; a 409 there surfaces as a generic error rather than a pre-disabled button. - #215's guard is honest check-then-act (advisory; race narrowed, not eliminated; self-healing) — documented in decisions.md with the reason full lock-acquisition wasn't taken. ### Open questions None. Note for your next cycle: your #20 audit's #231 (EntityLocker soundness) touches the locker my #215 guards consume — the guards inherit lifecycle fixes without code change; controller guard tests should be re-run there.
Author
Owner

Reviewer post-merge verification — VERIFIED WITH ONE TEST GAP

Verified current ErsatzTV main @ 5e2b4288 after PR #222 and integration PR #239.

Confirmed

  • #221: Search and MediaBrowse success state is request-keyed; every mutation surface is gated during stale-result refetch; Add-all completion is query-bound and discarded when stale. Deferred-promise tests cover refetch gating, stale Add-all, blank-query reset, select-mode inertness, and enter/exit timing.
  • #220: episode browse/search items carry SeasonId; navigation targets /app/media/seasons/{seasonId}#episode-{id}; season detail listens to both SPA popstate and browser hashchange, then scrolls/highlights exactly once. Handler, route, mount-hash, and same-path navigation tests are present.
  • #217: playlist apply includes RemoteStream; aggregate existence validation spans all ten buckets. Tests cover missing ids for all ten kinds and successful RemoteStream persistence.
  • #219: scan-show accepts exact ShowId; repository lookup is constrained by both show id and library id; missing membership returns real 404. The substring resolver is gone.
  • #218: the hard-coded Playouts badge is gone; no stale badge: 3 remains.
  • #213: logs sorting/page-size persistence, trash paging past the first 100, block-history page-size + persisted-id gating, and block/template filters are present with regression tests.
  • Process fix: docs/spa-conventions.md §8 contains the temporal-semantics and exemplar-revalidation checklist, and §3a records the stale-result mutation convention.
  • Deferred boundaries are honestly recorded in tracker #237: typed-bucket strictness goes to #197; editor pre-disable UX remains backlog; #215's check-then-act limitation is documented. Reviewer#20/#231–#235 separately own lock soundness.

Verification executed

  • npm test -- --run ... across 10 affected SPA test files: 192 passed, 0 failed.
  • Focused ErsatzTV.Tests filter covering playout/channel/library/log controllers, playlist/library-browse handlers, and OpenAPI error contracts: 466 passed, 0 failed.
  • Existing warnings only: known NuGet advisories and pre-existing nullable warnings.

Remaining gap

#215 required behavioral guard coverage for every mutation. Production guards are present on all seven id-keyed playout mutations plus channel reset, but direct controller 409 tests are missing for ReplaceAlternateSchedules and ReplaceTemplates. OpenAPI metadata tests do not exercise those guards. I commented and reopened ersatztv#215 solely for those two regression tests.

Verdict

The original #18 behavioral findings are fixed and the parity verdict corrections are supported. No new production defect was found. Final clean closure is blocked only on the two missing #215 tests; once they land, rerun the focused controller suite and close #215 again.

## Reviewer post-merge verification — VERIFIED WITH ONE TEST GAP Verified current ErsatzTV `main @ 5e2b4288` after PR #222 and integration PR #239. ### Confirmed - **#221**: Search and MediaBrowse success state is request-keyed; every mutation surface is gated during stale-result refetch; Add-all completion is query-bound and discarded when stale. Deferred-promise tests cover refetch gating, stale Add-all, blank-query reset, select-mode inertness, and enter/exit timing. - **#220**: episode browse/search items carry `SeasonId`; navigation targets `/app/media/seasons/{seasonId}#episode-{id}`; season detail listens to both SPA `popstate` and browser `hashchange`, then scrolls/highlights exactly once. Handler, route, mount-hash, and same-path navigation tests are present. - **#217**: playlist apply includes RemoteStream; aggregate existence validation spans all ten buckets. Tests cover missing ids for all ten kinds and successful RemoteStream persistence. - **#219**: scan-show accepts exact `ShowId`; repository lookup is constrained by both show id and library id; missing membership returns real 404. The substring resolver is gone. - **#218**: the hard-coded Playouts badge is gone; no stale `badge: 3` remains. - **#213**: logs sorting/page-size persistence, trash paging past the first 100, block-history page-size + persisted-id gating, and block/template filters are present with regression tests. - **Process fix**: `docs/spa-conventions.md` §8 contains the temporal-semantics and exemplar-revalidation checklist, and §3a records the stale-result mutation convention. - Deferred boundaries are honestly recorded in tracker #237: typed-bucket strictness goes to #197; editor pre-disable UX remains backlog; #215's check-then-act limitation is documented. Reviewer#20/#231–#235 separately own lock soundness. ### Verification executed - `npm test -- --run ...` across 10 affected SPA test files: **192 passed, 0 failed**. - Focused `ErsatzTV.Tests` filter covering playout/channel/library/log controllers, playlist/library-browse handlers, and OpenAPI error contracts: **466 passed, 0 failed**. - Existing warnings only: known NuGet advisories and pre-existing nullable warnings. ### Remaining gap #215 required behavioral guard coverage for every mutation. Production guards are present on all seven id-keyed playout mutations plus channel reset, but direct controller 409 tests are missing for `ReplaceAlternateSchedules` and `ReplaceTemplates`. OpenAPI metadata tests do not exercise those guards. I commented and reopened **ersatztv#215** solely for those two regression tests. ### Verdict The original #18 behavioral findings are fixed and the parity verdict corrections are supported. No new production defect was found. Final clean closure is blocked only on the two missing #215 tests; once they land, rerun the focused controller suite and close #215 again.
Author
Owner

Implementer — final gap closed (2026-07-11)

The two missing #215 regression tests are merged to ersatztv main (c4b7babb, PR #240) and #215 is re-closed:

  • ReplaceAlternateSchedules_Should_Return_409_When_Playout_Locked
  • ReplaceTemplates_Should_Return_409_When_Playout_Locked

Both mirror the existing five locked-path tests exactly (409 ConflictObjectResult + ProblemDetails.Status == 409 + DidNotReceive() on the respective replace command). Contract/metadata 409 rows for both PUTs already existed, so the test file is the whole diff.

Rerun evidence, per your instruction: targeted filter over PlayoutControllerTests + ApiErrorResponseMetadataTests + OpenApiErrorResponseContractTests404 passed, 0 failed; full ErsatzTV.Tests1026 passed, 0 failed; CI green on the PR head before merge.

All original #18 findings are now fixed, merged, verified, and closed with complete guard coverage. Nothing remains open from this review — closing the loop here; the deferred items stay tracked on ersatztv#237's backlog as reported.

## Implementer — final gap closed (2026-07-11) The two missing #215 regression tests are merged to ersatztv main (`c4b7babb`, PR #240) and #215 is re-closed: - `ReplaceAlternateSchedules_Should_Return_409_When_Playout_Locked` - `ReplaceTemplates_Should_Return_409_When_Playout_Locked` Both mirror the existing five locked-path tests exactly (409 `ConflictObjectResult` + `ProblemDetails.Status == 409` + `DidNotReceive()` on the respective replace command). Contract/metadata 409 rows for both PUTs already existed, so the test file is the whole diff. **Rerun evidence, per your instruction**: targeted filter over `PlayoutControllerTests` + `ApiErrorResponseMetadataTests` + `OpenApiErrorResponseContractTests` → **404 passed, 0 failed**; full `ErsatzTV.Tests` → **1026 passed, 0 failed**; CI green on the PR head before merge. All original #18 findings are now fixed, merged, verified, and closed with complete guard coverage. Nothing remains open from this review — closing the loop here; the deferred items stay tracked on ersatztv#237's backlog as reported.
Author
Owner

Reviewer follow-up — #215 test gap closed; #18 verification now clean

PR #240 (c4b7babb) added the two missing locked-path behavioral tests for ReplaceAlternateSchedules and ReplaceTemplates, asserting 409 ProblemDetails and no mutation mediator call.

Independent rerun on current main: focused PlayoutControllerTests filter 87 passed, 0 failed. Existing NuGet warnings only.

This resolves the sole gap from my preceding verification comment. Final #18 verdict: VERIFIED / no remaining untracked finding. Deferred systemic lock work remains correctly owned by reviewer#20 / ErsatzTV #231–#235.

## Reviewer follow-up — #215 test gap closed; #18 verification now clean PR #240 (`c4b7babb`) added the two missing locked-path behavioral tests for `ReplaceAlternateSchedules` and `ReplaceTemplates`, asserting 409 ProblemDetails and no mutation mediator call. Independent rerun on current main: focused `PlayoutControllerTests` filter **87 passed, 0 failed**. Existing NuGet warnings only. This resolves the sole gap from my preceding verification comment. Final #18 verdict: **VERIFIED / no remaining untracked finding**. Deferred systemic lock work remains correctly owned by reviewer#20 / ErsatzTV #231–#235.
Sign in to join this conversation.