Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit sweep (adversarial-reviewer #22/#23) — the parallel-safe, backend-isolated slice of #254. No API contract change (handler-internal only), no OpenAPI/SPA change.
What & why
audit#22 F4 — standardize post-commit enqueues on CancellationToken.None (20 handlers)
Handlers under MediaCollections/, ProgramSchedules/, Playouts/, Channels/ threaded the request cancellationToken into work that runs afterSaveChangesAsync commits — the WriteAsync rebuild/refresh enqueue, mediator.Publish, search reindex, cache Refresh, and post-commit lookups that gate an enqueue. A late HTTP-client disconnect cancels that token, so an already-durable commit throws on the way out and silently drops its side effect (the playout rebuild is never queued → the persisted edit never takes visible effect). This generalizes the #251 deco-handler fix to the codebase.
Excluded (deliberate):
BuildPlayoutHandler — background/worker handler; its token is the worker shutdown token, not a client-disconnect token, so honoring cancellation on its downstream enqueues is correct.
UpdateFFmpegSettingsHandler + the two Configuration/ settings handlers — multi-Upsert commit with an interleaved enqueue; the "when is it committed" partial-commit question is broader than this clean single-SaveChanges pattern → separate follow-up.
Response-projection reloads (ReplaceProgramScheduleItemsHandler/AddProgramScheduleItemHandler) correctly keep the request token — a cancelled response after a durable commit + None-enqueue loses nothing.
DeleteChannelHandler/DeletePlayoutHandler deleted the channel-guide {number}.xml via staticFile.Delete (escaped MockFileSystem) after commit (orphans the xml on a crash — nothing reaps it; GetChannelGuideHandler serves the whole cache folder). Now routed through IFileSystem.File.Delete and done before the commit; the xml is regenerable on demand, so a pre-commit delete is the safe ordering.
audit#23 F4 — Max()-on-empty guard
ReplacePlayoutAlternateScheduleItemsHandler now rejects an empty item list in the handler (not only the controller pre-guard), so a direct caller (MCP/test/reuse) gets a clean 422 instead of the Max()-on-empty crash. Deletes the stale // TODO: validate that items is not empty.
Deferred (documented in the issue, not in this PR)
F6 dirty-nav guard (frontend editor-chrome → #230/#238/#248), F1 reindex reconcile (optional self-heal), F7 empty-list semantics + #21-F2 block/template immediacy (→ #197-prep docs), and the config/FFmpeg multi-upsert token case.
Docs
docs/api-conventions.md §7a — the post-commit CancellationToken.None convention + the two boundaries (response-projection keeps the request token; background handlers keep the worker token).
docs/decisions.md — rationale, exact sweep scope, and a coordination note for #253 PR2–PR4 (they add Version++ pre-commit to several of the same handlers — different line region, low conflict; merge main in and expect the None convention already present).
Tests
DeleteChannelHandlerTests / PlayoutHandlerTests — guide-cache file is deleted through the FS abstraction (seeded in MockFileSystem, asserted gone) for both delete handlers.
PlayoutHandlerTests — empty alternate-schedule-items list → BaseError.
3 new tests pass. No pre-commit SaveChangesAsync token was flipped (grep-verified).
Closes#254 (scoped slice; deferred items remain tracked on the issue).
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit sweep (adversarial-reviewer #22/#23) — the parallel-safe, backend-isolated slice of #254. No API contract change (handler-internal only), no OpenAPI/SPA change.
## What & why
### audit#22 F4 — standardize post-commit enqueues on `CancellationToken.None` (20 handlers)
Handlers under `MediaCollections/`, `ProgramSchedules/`, `Playouts/`, `Channels/` threaded the request `cancellationToken` into work that runs **after** `SaveChangesAsync` commits — the `WriteAsync` rebuild/refresh enqueue, `mediator.Publish`, search reindex, cache `Refresh`, and post-commit lookups that **gate** an enqueue. A late HTTP-client disconnect cancels that token, so an *already-durable* commit throws on the way out **and silently drops its side effect** (the playout rebuild is never queued → the persisted edit never takes visible effect). This generalizes the #251 deco-handler fix to the codebase.
**Excluded (deliberate):**
- `BuildPlayoutHandler` — background/worker handler; its token is the worker shutdown token, not a client-disconnect token, so honoring cancellation on its downstream enqueues is correct.
- `UpdateFFmpegSettingsHandler` + the two `Configuration/` settings handlers — multi-`Upsert` commit with an interleaved enqueue; the "when is it committed" partial-commit question is broader than this clean single-`SaveChanges` pattern → separate follow-up.
- Response-projection reloads (`ReplaceProgramScheduleItemsHandler`/`AddProgramScheduleItemHandler`) correctly keep the request token — a cancelled response after a durable commit + `None`-enqueue loses nothing.
### audit#22 F2 — guide-xml delete: FS-abstraction + pre-commit ordering
`DeleteChannelHandler`/`DeletePlayoutHandler` deleted the channel-guide `{number}.xml` via **static** `File.Delete` (escaped `MockFileSystem`) **after** commit (orphans the xml on a crash — nothing reaps it; `GetChannelGuideHandler` serves the whole cache folder). Now routed through `IFileSystem.File.Delete` and done **before** the commit; the xml is regenerable on demand, so a pre-commit delete is the safe ordering.
### audit#23 F4 — `Max()`-on-empty guard
`ReplacePlayoutAlternateScheduleItemsHandler` now rejects an empty item list in the handler (not only the controller pre-guard), so a direct caller (MCP/test/reuse) gets a clean 422 instead of the `Max()`-on-empty crash. Deletes the stale `// TODO: validate that items is not empty`.
## Deferred (documented in the issue, not in this PR)
F6 dirty-nav guard (frontend editor-chrome → #230/#238/#248), F1 reindex reconcile (optional self-heal), F7 empty-list semantics + #21-F2 block/template immediacy (→ #197-prep docs), and the config/FFmpeg multi-upsert token case.
## Docs
- `docs/api-conventions.md` §7a — the post-commit `CancellationToken.None` convention + the two boundaries (response-projection keeps the request token; background handlers keep the worker token).
- `docs/decisions.md` — rationale, exact sweep scope, and a coordination note for **#253 PR2–PR4** (they add `Version++` pre-commit to several of the same handlers — different line region, low conflict; merge `main` in and expect the `None` convention already present).
## Tests
- `DeleteChannelHandlerTests` / `PlayoutHandlerTests` — guide-cache file is deleted **through the FS abstraction** (seeded in `MockFileSystem`, asserted gone) for both delete handlers.
- `PlayoutHandlerTests` — empty alternate-schedule-items list → `BaseError`.
## Verification
- `dotnet build` Application + Tests: 0 errors (warnings-as-errors clean on Application).
- Affected handler tests (Application.{Channels,Playouts,MediaCollections,ProgramSchedules,Scheduling}): **126 passed**.
- Integration + Playout/Channel controller tests: **123 passed**.
- 3 new tests pass. No pre-commit `SaveChangesAsync` token was flipped (grep-verified).
Closes #254 (scoped slice; deferred items remain tracked on the issue).
timothy
added the review label 2026-07-11 17:43:30 +02:00
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit
sweep (adversarial-reviewer #22/#23), the parallel-safe backend-isolated slice.
audit#22 F4 — standardize post-commit enqueues on CancellationToken.None:
20 command handlers under MediaCollections/, ProgramSchedules/, Playouts/,
Channels/ threaded the request cancellationToken into work that runs AFTER
SaveChangesAsync commits (WriteAsync rebuild/refresh enqueues, mediator.Publish,
reindex, cache Refresh, and post-commit lookups that gate an enqueue). A late
client-disconnect then turns an already-durable commit into a thrown request AND
drops the side effect. Generalizes the #251 deco-handler fix. Excludes
BuildPlayoutHandler (worker/background token, not a client-disconnect token),
the config/FFmpeg multi-upsert handlers (partial-commit case, separate
follow-up), and response-projection reloads (correctly keep the request token).
audit#22 F2 — DeleteChannelHandler/DeletePlayoutHandler now delete the channel
guide {number}.xml through IFileSystem.File.Delete (observable under
MockFileSystem) and BEFORE the commit (a post-commit delete orphans the xml on a
crash; the xml is regenerable on demand, so pre-commit delete is the safe order).
audit#23 F4 — ReplacePlayoutAlternateScheduleItemsHandler rejects an empty item
list in the handler (not only the controller pre-guard) so a direct caller can't
trip the Max()-on-empty crash.
Docs: api-conventions.md §7a (post-commit token convention + boundaries),
decisions.md entry (rationale, sweep scope, #253 PR2-4 coordination note).
Tests: guide-cache-delete-through-FS for both delete handlers, empty-list guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independent adversarial review of PR #266 found two sibling handlers with the
identical post-commit `_smartCollectionCache.Refresh(cancellationToken)` pattern
that the sweep missed (only UpdateSmartCollection was caught). Same audit#22 F4
class: a late client-disconnect after the commit lands would throw and leave the
in-memory smart-collection cache stale vs the committed DB. → CancellationToken.None.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit sweep (adversarial-reviewer #22/#23) — the parallel-safe, backend-isolated slice of #254. No API contract change (handler-internal only), no OpenAPI/SPA change.
What & why
audit#22 F4 — standardize post-commit enqueues on
CancellationToken.None(20 handlers)Handlers under
MediaCollections/,ProgramSchedules/,Playouts/,Channels/threaded the requestcancellationTokeninto work that runs afterSaveChangesAsynccommits — theWriteAsyncrebuild/refresh enqueue,mediator.Publish, search reindex, cacheRefresh, and post-commit lookups that gate an enqueue. A late HTTP-client disconnect cancels that token, so an already-durable commit throws on the way out and silently drops its side effect (the playout rebuild is never queued → the persisted edit never takes visible effect). This generalizes the #251 deco-handler fix to the codebase.Excluded (deliberate):
BuildPlayoutHandler— background/worker handler; its token is the worker shutdown token, not a client-disconnect token, so honoring cancellation on its downstream enqueues is correct.UpdateFFmpegSettingsHandler+ the twoConfiguration/settings handlers — multi-Upsertcommit with an interleaved enqueue; the "when is it committed" partial-commit question is broader than this clean single-SaveChangespattern → separate follow-up.ReplaceProgramScheduleItemsHandler/AddProgramScheduleItemHandler) correctly keep the request token — a cancelled response after a durable commit +None-enqueue loses nothing.audit#22 F2 — guide-xml delete: FS-abstraction + pre-commit ordering
DeleteChannelHandler/DeletePlayoutHandlerdeleted the channel-guide{number}.xmlvia staticFile.Delete(escapedMockFileSystem) after commit (orphans the xml on a crash — nothing reaps it;GetChannelGuideHandlerserves the whole cache folder). Now routed throughIFileSystem.File.Deleteand done before the commit; the xml is regenerable on demand, so a pre-commit delete is the safe ordering.audit#23 F4 —
Max()-on-empty guardReplacePlayoutAlternateScheduleItemsHandlernow rejects an empty item list in the handler (not only the controller pre-guard), so a direct caller (MCP/test/reuse) gets a clean 422 instead of theMax()-on-empty crash. Deletes the stale// TODO: validate that items is not empty.Deferred (documented in the issue, not in this PR)
F6 dirty-nav guard (frontend editor-chrome → #230/#238/#248), F1 reindex reconcile (optional self-heal), F7 empty-list semantics + #21-F2 block/template immediacy (→ #197-prep docs), and the config/FFmpeg multi-upsert token case.
Docs
docs/api-conventions.md§7a — the post-commitCancellationToken.Noneconvention + the two boundaries (response-projection keeps the request token; background handlers keep the worker token).docs/decisions.md— rationale, exact sweep scope, and a coordination note for #253 PR2–PR4 (they addVersion++pre-commit to several of the same handlers — different line region, low conflict; mergemainin and expect theNoneconvention already present).Tests
DeleteChannelHandlerTests/PlayoutHandlerTests— guide-cache file is deleted through the FS abstraction (seeded inMockFileSystem, asserted gone) for both delete handlers.PlayoutHandlerTests— empty alternate-schedule-items list →BaseError.Verification
dotnet buildApplication + Tests: 0 errors (warnings-as-errors clean on Application).SaveChangesAsynctoken was flipped (grep-verified).Closes #254 (scoped slice; deferred items remain tracked on the issue).