ElasticSearchIndex.UpdateSong has no regression test — an Elastic-only reintroduction of the #701 mutation stays CI-green #824
Closed
opened 2026-08-23 00:07:30 +02:00 by timothy
·
2 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
No labels
priority: medium
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#824
Reference in New Issue
Block a user
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.
Deferred from #701 and named there rather than papered over.
The gap
#701 removed
metadata.Artists ??= []/AlbumArtists ??= []from both search indexers, but onlyLuceneSearchIndexgained a regression test (ErsatzTV.Tests/Integration/SongIndexerMetadataMutationTests).ElasticSearchIndex.UpdateSongholds an independent copy of the same logic, so reintroducing the mutation there alone leaves the whole suite green.The Lucene fixture is not extendable as-is: it injects a Lucene
IndexWriterinto a private field, whereasElasticSearchIndexneeds anElasticsearchClient, i.e. a live server or a stubbed transport.Why it was not done in #701
The alternative on the table was a source-text guard asserting the string
??=does not appear near those fields. That was rejected: the??=idiom is correct on navigation collections and appears 92 times across the app projects, so a text guard is either noisy or so narrowly scoped that it stops matching the day someone reformats the line. A brittle guard that reads as coverage is the failure#781anddocs/defect-shapes-773.md§5.3 warn about.Options
ElasticsearchClienttransport and run the same tracked-entity assertions.fix-the-boundary-not-the-siteshape and is probably the better answer — but it is a refactor of two large files and wants its own review.Prefer whichever actually executes; do not close this with a test that cannot be shown to go red.
Done-when
ElasticSearchIndex.UpdateSongis covered by a test that is WITNESSED RED when the??=mutation is restored (quote the red)Claiming — bundled session closing out both #701 deferrals (#823 + #824) together, because both must edit the same decision record (
media.nullable-primitive-collection-mutation) and splitting them across two sessions would produce conflicting edits to itsrule/mechanicstext.Claude Code / Opus 5, orchestrator tier. Work happens in a worktree off
origin/main.Closing record
Outcome: Shipped in PR #879 (merged as
1afad0851).ElasticSearchIndex.UpdateSongis now covered byErsatzTV.Tests/Integration/ElasticSongIndexerMetadataMutationTests.cs, which drives the real indexer against a realTvContext. No production change toElasticSearchIndex.cs— this was coverage only.Root cause: #701 removed
metadata.Artists ??= []from both search indexers but only Lucene gained a fixture, so an Elastic-only reintroduction left the whole suite green. Not a code defect — a coverage hole that nothing could notice.Decisions/conventions changed:
media.nullable-primitive-collection-mutation'smechanics:now describes the shipped Elastic fixture and its two traps instead of saying the gap exists.ThrowOnWarningLoggerlifted toErsatzTV.Tests/Supportso both fixtures share it.Reusable knowledge:
Elastic.Transport.InMemoryRequestInvokeris public in the pinned version, andElasticsearchClientSettings(NodePool, IRequestInvoker)accepts it — so an Elasticsearch-backed component is testable with no server, no socket and no new package, injected into the private_clientthe way #701 injects the LuceneIndexWriter.UpdateItemsnever runs_client ??= CreateClient(), so the injected instance is the one used. An initial recon pass reported this as UNPROVEN; enumerating the DLL's types directly refuted that.X-Elastic-Product: Elasticsearch. The client runs a product check and otherwise throwsUnsupportedProductExceptionintoUpdateSong's catch — which would have made the fixture a green measurement of the error path. That is exactly how it first failed, caught by theThrowOnWarningLoggerrather than by luck. An empty response body fails the same way.ShouldContain(songId)passes off the index name once that name carries digits, so it silently stops discriminating.SearchIndexMutationCoverageTestsderives theISearchIndexpopulation from the declaring assembly, so a third indexer cannot reopen this hole unnoticed. Its claim stops where the check does — no static check can prove a named fixture actually drives its indexer, so it forces a human to look rather than proving coverage.Verification: The mutation was executed, not asserted: restoring
??=inElasticSearchIndexalone reddens the new fixture onmetadata.Artists should be null but was []while the Lucene fixture stays green — the #824 hole demonstrated rather than described. The mirror was also witnessed: restoring it inLuceneSearchIndexalone reddens Lucene and leaves Elastic green, confirming the shared-logger extraction did not cost #701 its proof and that the two fixtures are independently load-bearing. Three further mutations redden the coverage guard (indexer dropped from the covered set; two indexers mapped to one fixture; a fixture declaring no runnable test).Deferred: none.
Docs updated:
docs/decisions/records/media/nullable-primitive-collection-mutation.md,docs/decisions/README.md(regenerated),docs/guard-inventory.md.