fix(403): accurate Classic fallback message + engine-coverage tripwire
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 8s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m20s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 9s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 14m21s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14m18s

Review follow-ups (cold review of PR #457):
- Marathon can reach PlayoutBuilder's default arm via `goto default` when
  its enumerator can't be built; the new warning claimed "Marathon is not
  supported by classic scheduling", which is false. Distinguish
  supported-but-failed-to-build (logs "could not build") from genuinely
  unsupported using PlaybackOrderSupport, which also makes Classic a
  runtime consumer of the matrix.
- PlaybackOrderSupportTests now asserts every SchedulingEngineKind has a
  matrix entry, so a new engine kind fails the test instead of throwing
  KeyNotFoundException at runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 18:32:14 +02:00
co-authored by Claude Opus 4.8
parent 0f34c86afa
commit 1d95cbfee6
2 changed files with 31 additions and 6 deletions
@@ -28,6 +28,20 @@ public class PlaybackOrderSupportTests
}
}
// Every SchedulingEngineKind must have a matrix entry, or Matrix[engine] throws KeyNotFoundException at
// runtime instead of failing here. This is the engine-axis counterpart to the order tripwire.
[Test]
public void EveryEngineKind_HasAMatrixEntry()
{
var classified = PlaybackOrderSupport.Engines.ToHashSet();
foreach (SchedulingEngineKind engine in Enum.GetValues<SchedulingEngineKind>())
{
classified.ShouldContain(engine,
$"SchedulingEngineKind.{engine} has no PlaybackOrderSupport matrix entry (#403).");
}
}
// The two sets must partition the enum: no order both supported and unsupported, and together they cover
// exactly the enum (no stale entry for a removed value, no missing value).
[Test]
+17 -6
View File
@@ -1459,12 +1459,23 @@ public class PlayoutBuilder : IPlayoutBuilder
goto default;
default:
// An order the classic engine doesn't handle (#403). Say so instead of silently substituting a
// different, arbitrary-looking order; keep airing on the Random fallback so the channel does not
// go dark on one misconfigured item.
_logger.LogWarning(
"Playback order {PlaybackOrder} is not supported by classic scheduling; falling back to Random",
playbackOrder);
// Say so instead of silently substituting a different, arbitrary-looking order; keep airing on
// the Random fallback so the channel does not go dark on one misconfigured item (#403). A
// supported order can also land here via `goto default` (Marathon that couldn't build its
// enumerator) -- distinguish the two so the log doesn't claim a supported order is unsupported.
if (PlaybackOrderSupport.IsSupported(SchedulingEngineKind.Classic, playbackOrder))
{
_logger.LogWarning(
"Could not build a {PlaybackOrder} enumerator for classic scheduling; falling back to Random",
playbackOrder);
}
else
{
_logger.LogWarning(
"Playback order {PlaybackOrder} is not supported by classic scheduling; falling back to Random",
playbackOrder);
}
return new RandomizedMediaCollectionEnumerator(mediaItems, state);
}
}