e87285c33de783d86dae381aff0c217a6d35722b
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0081bed4b1 |
docs(460): drop the "clean at rest" overstatement from the test header
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 14s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 25s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m51s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Follow-up to the cold review of 615e00a. The record was corrected to say only that no NEW sentinel rows are created, but the test file's header still carried the disclaimed "the data is clean at rest for every provider" claim — sitting on the very test that supposedly proved it. It now states what the tests actually pin, and names what they cannot speak to (rows written before this change, which keep the sentinel and are covered only by the read coercion). Also corrects docs/testing.md's ErsatzTV.Core.Tests count (543 -> ~650 measured; the neighbouring Scanner.Tests cell is corrected in #602 instead, to keep the two open PRs off the same line). |
||
|
|
890745d1d4 |
fix(460): write null LastScan on disable-sync instead of the MinValue sentinel [decisions-edit]
MediaSourceRepository's Plex/Jellyfin/Emby remove-and-recreate (disable-sync) flows stamped SystemTime.MinValueUtc into library.LastScan, so normal use kept minting 0001-01-01 sentinel rows. #409 fixed the READ side (the API coerces the sentinel to null and a migration cleaned the historical residue), so the wire contract was already correct — this stops new sentinel rows being written. Safe because every remaining LastScan reader either coalesces (LastScan ?? SystemTime.MinValueUtc) for its own non-nullable scan-comparison needs, or is the API read-boundary coercion itself — audited every reference under ErsatzTV{,.Application,.Core,.Infrastructure,.Scanner,.Mcp} plus web/. There is no Where/OrderBy/GroupBy on LastScan anywhere, so the SQL null-ordering divergence between SQLite and MySQL has no surface here. Scan-comparison behavior is unchanged. Deliberately ships NO second cleanup migration: rows written between #409's NullOutNeverScannedLastScan and this change keep the sentinel at rest, and the permanent read coercion — not a migration — is what keeps the contract honest for them (as it must be anyway for a restored or hand-edited DB). LibraryPath.LastScan is likewise left untouched: the flows re-add Paths with their original values, and its only readers are the local-library scan handlers, so it has no API surface and no remote-scan effect. Regression test per provider (MediaSourceRepositoryDisableSyncTests), proven non-vacuous: restoring the MinValue writes fails all three. [decisions-edit] — the media.lastscan-null-boundary record documented this write as an ONGOING sentinel source and rested its "the coercion is permanent" argument on it, so the rationale prose is corrected in the same PR per docs.decision-lifecycle. The Rule line is unchanged, so the generated decisions/README.md catalog is byte-identical. MediaSourceRepository.cs also loses its UTF-8 BOM (fix-as-you-touch, #311). fixes #460 |
||
|
|
2f2bcca681 |
fix(500): dedup incoming metadata collections so a duplicate name inserts once
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 9s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m24s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m27s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 21m31s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
The remove-stale + add-new reconcile idiom materializes its add set with
.ToList() BEFORE the loop mutates the existing collection, so the add filter
(`incoming.All(x2 => x2.Name != x.Name)`) is evaluated against a snapshot. Two
identically-named incoming entries whose name is not yet on the existing item
therefore BOTH passed the filter and BOTH inserted — a duplicate row.
Deduplicate the incoming set on the same key the filter compares (Name; Guid
for Guids), in both copies of the idiom:
- PlexMovieLibraryScanner.UpdateMetadata (the original) — genres, studios,
actors, directors, writers, guids, tags.
- JellyfinMusicVideoLibraryScanner.Reconcile{Genres,Tags,Studios,Artists}
(added in #497, mirrors the Plex pattern verbatim).
Plex ACTORS are the exception and get an artwork-preferring dedup hoisted out
and shared with the remove filter, because that filter is keyed on
(Name, artwork-presence) — it is the mechanism that drops an artwork-less actor
so the add loop can re-add it WITH artwork. A bare DistinctBy(a => a.Name)
there keeps the FIRST duplicate, so Plex listing the artwork-less copy first
discarded the artwork; worse, the remove filter would still see the
artwork-less duplicate, making its upgrade clause false, so the stale row was
never removed and the artwork never arrived on ANY later scan either. Actor
also carries Role/Order, which first-wins would silently drop too. Caught by
the cold review of the first version of this commit.
For the Jellyfin scanner the dedup sits at the incoming-list declaration, which
also covers the remove filter — safe because all four of those filters only ask
"is this name present at all", an answer duplicates cannot change.
Tests: duplicate-collapse for both paths, the two Actors cases above, and a
POSITIVE CONTROL proving distinct entries are still all added and stale ones
still removed (without it, a mis-keyed dedup that collapsed genuinely different
entries would pass every other assertion). Each proven non-vacuous.
The Plex tests drive the protected UpdateMetadata through a minimal test-only
subclass, as MediaServerMovieLibraryScannerTests already does.
Low likelihood in practice (a media server emitting two identically-named
genres for one item is unusual); this is defensive, with no observed occurrence.
The same idiom is copied into 8 further scanners/repositories that this change
deliberately does not touch (the issue scoped it to two paths) — filed as #600
so the class of bug is tracked rather than silently left in the majority of its
instances. The dedup rule is recorded under scan.musicvideo-reconciliation.
fixes #500
|
||
|
|
f54c9ae195 |
refactor(395): dedup Scripted≡YAML enumerator construction into ContentEnumeratorBuilder
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 9s
PR Gates / Docs update reminder (pull_request) Successful in 13s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m25s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m36s
PR Gates / decisions lifecycle (pull_request) Failing after 14s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been cancelled
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Has been cancelled
Extract the byte-identical PlaybackOrder -> IMediaCollectionEnumerator switch shared by SchedulingEngine.EnumeratorForContent (Scripted) and EnumeratorCache.GetEnumeratorForContent (Sequential/YAML) into one static per-family seam, mirroring #380's ShuffleSourceBuilder. Each engine keeps its own "not supported" warning on the None branch, so the per-engine message is unchanged. Adds ContentEnumeratorBuilderTests pinning the block-shuffle-not-classic trap and the unsupported-order -> None (#70) contract across all 8 unsupported orders. Corrects the testing.scripted-playout-golden-deferred decision record: Scripted's external-process + HTTP pipeline is integration-only (deferred to #563), but the in-process SchedulingEngine it drives IS unit-testable (ScriptedScheduleController is a 1:1 pass-through) -- the earlier "un-golden-able by construction" framing conflated transport with engine. docs/testing.md reframed to match. [decisions-edit] fixes #395 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
80a9824a4e |
test(381): golden coverage for Sequential (YAML) playout builder; document Scripted deferral
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 28s
PR Gates / Docs update reminder (pull_request) Successful in 46s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m53s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 17s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 18s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m29s
PR Gates / decisions lifecycle (pull_request) Successful in 18s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 21m0s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Extends the #163 PlayoutBuildGoldenTests in-memory net to the Sequential (YAML) builder: a committed fixture (Goldens/Fixtures/sequential-schedule.yml) with two `count: 2` instructions over one chronological collection, built via SequentialPlayoutBuilder over the pinned window. The count/all/duration handlers do UTC-only arithmetic off the caller-supplied start, so the case is TZ-independent (passes, not skips, under a non-UTC TZ) and needs no Assume guard. Non-vacuity: a fixture count tweak flips the golden + the contiguity assertion. Scripted is deliberately excluded from the golden net — ScriptedPlayoutBuilder shells out via Cli.Wrap to an external process that drives SchedulingEngine over HTTP, which no in-memory golden can characterize. Recorded as the Done-when "documented decision" arm in docs/decisions.md (testing.scripted-playout-golden-deferred) + docs/testing.md; the scripted integration harness is tracked as follow-up #563. fixes #381 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
71708633f5 |
test(264): pin the path-scan-error log; correct testing.md accuracy
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 38s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 10m49s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 15m6s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m9s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Re-review of the previous fix commit (
|
||
|
|
986ccfaf6c |
fix(264): log local path scan errors (review finding) + map Scanner.Tests
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 6s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 8s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 8s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 47s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m28s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m3s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Adversarial review (MERGEABLE, no blockers) raised two items worth folding in rather than deferring: Medium — ScanLocalLibraryHandler silently swallowed path scan errors, while the three remote scanners it mirrors all log result.LeftToSeq(). That mattered less when a failed path only skipped the path-level LastScan, but the previous commit makes a failed path suppress the library-level scan time too — so the user would see exactly the #264 symptom ("Never scanned") with nothing in the log explaining why. That is a diagnosis dead-end of the same class as the bug being fixed, so log it here rather than file a follow-up. No test: the sibling Synchronize*LibraryByIdHandlerTests don't assert on logging either, and LogError is an extension method that NSubstitute can't cleanly verify. Low — docs/testing.md bills itself as the authoritative map of what each test project covers but omitted ErsatzTV.Scanner.Tests entirely (1471 pre-existing tests). This PR adds a file to that project, so add the row and include it in the per-PR verification gate. |
||
|
|
7e6b9d2747 |
test(77): characterize clock-boundary schedule padding; docs
#77's core (pad/snap schedules to :00/:15/:30 via filler) already exists — FillerMode.Pad + PadToNearestMinute (Classic), pad_to_next/pad_until (Sequential); Block is inherently time-anchored. No production code change; this locks the behaviour end-to-end and documents that it exists. - PlayoutBuildGoldenTests.Classic_clock_padded: a PostRoll FillerMode.Pad(15) preset through the real PlayoutBuilder snaps content to :15 (golden + explicit quarter-hour assertion). Splits Verify -> CompareGolden for reuse. - ChannelGuideProjectorClockPadTests: the guide projection coalesces trailing filler so programmes STOP on the padded boundary (XMLTV half). - docs: decisions.md (2026-07-17 entry), domain-model.md (clock-boundary row), testing.md (test map + count 540->542). Deferred (UI, blocked on #388): one-click per-channel/schedule clock-align toggle + 60-min increment option. Refs #77 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dc5d889ccb |
test(163): golden characterization tests for playout building (Classic + Block)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 9s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 44s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 3m56s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 9m41s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 11m33s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
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> |
||
|
|
0290594f0b |
docs: testing map + generated endpoint index (#185)
Adds docs/testing.md as the authoritative testing map (consolidated from docs/contributing.md §8, now shrunk to a pointer), and a generated docs/endpoint-index.md via scripts/generate-endpoint-index.py (hooked into scripts/update-openapi.sh). Updates docs/README.md's reading order and removes the "still to come" placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |