Commit Graph
6 Commits
Author SHA1 Message Date
timothyandClaude Fable 5.1 c0a70d724a test(563): witness the Newtonsoft half of the binder claim, and narrow the prose to what reddens
The branch asserted that binding fixture bodies through ApiJsonSettings makes "a swap to a
lookalike serializer" redden. Measured, only half of that was true: replacing
ScriptedScheduleControllerTests' BodyBinderSettings with a plain `new JsonSerializerSettings()`
-- a Newtonsoft lookalike that has lost the production configuration -- left all 8 tests green.
Only the System.Text.Json swap reddened. So the production edits the branch makes for that
coupling (ErsatzTV/Serialization/ApiJsonSettings.cs and the Startup rewrite) were justified in
four places by a hazard no test could see.

Both halves are now real. Production_Body_Binder_Keeps_Declared_Defaults_Over_An_Explicit_Null
binds `{"order": null}` and posts it to AddCollection: NullValueHandling.Ignore keeps
ContentCollection.Order at its declared "shuffle" and the call is a 200, where Newtonsoft's own
Include default writes the null through and AddCollection's Enum.TryParse returns a 400. That is
a behaviour difference a script would see, not a settings-shape assertion, so it is not a second
copy of the settings list.

Mutants, run 2026-09-05 over the 9-test fixture:

  Bind -> System.Text.Json web defaults      2 red
  BodyBinderSettings -> new()                1 red (was 0 before this commit)
  OpenApi RuntimeSettings -> new()           4 red (write side, naming strategy)

What still nothing observes is Startup.ConfigureServices itself: re-inlining the
AddNewtonsoftJson lambda as a hand-copy of Apply reddens no test, because a byte-equal mirror is
behaviourally indistinguishable. ApiJsonSettings removes the duplicate rather than detecting its
drift, and docs/testing.md, the decision record and all four docstrings now say that instead of
claiming a detector. Drift confined to ReferenceLoopHandling or the StringEnumConverter is
witnessed by neither suite; that is stated rather than left implied.

Also files the 401 blind spot the record had described as "tracked separately" while nothing
tracked it. ersatztv#913 records the chain, verified from source: the filter is registered
globally, EndpointRequiresKey fail-closes every mutating verb, ScriptedScheduleController carries
no [SkipApiAuthorization], and neither ScriptedPlayoutBuilder nor entrypoint.py supplies a
credential.

Refs ersatztv#563 and ersatztv#913.

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00
timothyandClaude Fable 5.1 b6ac7febbe style(563): rewrap the residue paragraph in the ScriptedScheduleControllerTests docstring
No content change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00
timothyandClaude Fable 5.1 4bd5cf9e91 docs(563): scope the required-member claim to the serializer, and name model validation as residue
The previous commit said a body omitting a `required` member "reaches the action" in production. That
overreaches what was measured: MVC adds an implicit required check for non-nullable reference types
(ErsatzTV.Core.Nullable has <Nullable>enable</Nullable>, and Startup configures no ApiBehaviorOptions,
so the [ApiController] automatic 400 is live), which would very likely reject that body before the
action. What is measured is the SERIALIZER: Newtonsoft deserializes it to a default, System.Text.Json
throws. The prose in the test, ApiJsonSettings, the record and docs/testing.md now stops there and puts
MVC model validation on the uncovered-wrapper list where it belongs.

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00
timothyandClaude Fable 5.1 16a6e2790f test(563): bind the fixture with the production body binder, and name the wrapper that is left
The docstring, the decision record and docs/testing.md all claimed the replay covered everything
except two hops. MVC model binding was a third: production binds /api/* bodies with Newtonsoft
(Startup -> AddNewtonsoftJson -> CustomContractResolver + StringEnumConverter) while the replay
deserialized with System.Text.Json. Measured on this tree: for the fixture's own bodies the two
agree, but for a body omitting the `required` member "collection" they diverge -- System.Text.Json
throws, Newtonsoft binds Collection = null and the action runs. So the fixture's stated purpose
("field names and casing match what the HTTP body binder accepts") was asserted by nothing, and a
fixture production would bind differently could still go green.

Rather than only widening the residue list, bind the way production binds. The registration moves
into ErsatzTV/Serialization/ApiJsonSettings.cs, Startup applies it from there, and both
OpenApiSerializerContractTests (which had its own mirror of the settings) and the scripted replay
now call that same function -- one definition, no copies to drift.
Production_Body_Binder_Ignores_Required_Members asserts both halves of the divergence THROUGH the
replay's own Bind helper, so pointing the replayer at another serializer reddens; the fixture's own
bodies cannot witness that swap.

The residue is now named honestly in all four places: the binding WRAPPER (input formatter, the
[ApiController] automatic 400 before an action runs) is uncovered, the serializer inside it is not.

Decisions-Edit: yes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00
timothyandClaude Fable 5.1 b3bc3ed115 test(563): pin per-item guide-group advancement, which mutant b showed was unmeasured
Removing SchedulingEngine.AddCountInternal's _state.AdvanceGuideGroup() left both
guide-group assertions green: the locked-group test only compared inside/outside
the group, and the snapshot only compared item 2 to item 0. Assert the actual
sequence instead.

Refs #563

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00
timothyandClaude Fable 5.1 02ecd484ed test(563): characterize the scripted engine build API in-process and replay a committed script fixture through the real controller
#381 deferred Scripted from the playout golden net because ScriptedPlayoutBuilder
shells out via Cli.Wrap to a user-authored program that drives SchedulingEngine
over HTTP loopback. #563 offered two arms: a full process+Kestrel integration
harness, or expanded engine coverage with the shell-out scoped out.

This takes the second arm, but delivers the first arm's "documented in-process
stand-in" so the scope-out is a measured claim rather than a prose one:

- SchedulingEngineTests grows from 1 test to 21, covering AddCollection/AddCount/
  AddAll/AddDuration/PadUntilExact, EPG guide-group locking, per-item history,
  the 20-call no-progress halt and its reset, and the anchor round-trip a
  Continue build restores from. Unknown-content-key cases assert false AND that
  nothing was scheduled.
- ScriptedScheduleControllerTests replays Fixtures/scripted-build.json through
  the real ScriptedScheduleController + ScriptedPlayoutBuilderService.MockSession
  + SchedulingEngine and pins a 13-item snapshot in the golden line format, so
  there is exactly one action->engine mapping under test — the production one. It
  also pins the three mappings the predecessor record's "1:1 pass-through"
  wording hides: 404 on an unknown build id, 400 on an unparseable playback
  order, a SILENT fall back to FillerKind.None on an unparseable filler kind, and
  the InvalidOperationException -> 400 translation.

MockSession was declared on IScriptedPlayoutBuilderService with zero callers;
it is the seam this needs and now has one.

Both fixtures are TZ-independent by construction (Chronological order plus only
instant-preserving instructions) and verified passing, not skipping, under
TZ=UTC, America/New_York, Australia/Lord_Howe and Asia/Kathmandu.

Refs #563

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
2026-09-05 09:25:11 +02:00