test(pins): widen whitelist for the episode-collections JSON shapes (item 2 series)

TestContractPin_CollectionsJSON tripped on the additive keys the
episode-collections work introduced — the tripwire worked exactly as
designed (additive keys must be whitelisted here AND in
docs/contract_pins.md first).

- Collections envelope gains rule_match_mode (item 2.4): the
  smart-collection match semantics on GET /api/collections/:id.
- BookInfo gains four STABLE keys (item 2.3): is_episode,
  feed_title, download_state, hidden — dropped omitempty so every
  card surface emits the same key set (the viewer_percentage
  precedent; adapters that predate episode awareness emit zero
  values).

docs/contract_pins.md changelog entry written. Nothing removed,
nothing renamed.
This commit is contained in:
John O'Keefe committed 2026-10-30 17:25:22 -04:00
1 parent 962f9089e0
commit bc91ce68d4
3 files changed
+24 -5

No files matched your search

+3
View File
@@ -459,6 +459,8 @@ var bookInfoKeys = []string{
"media_item_id", "title", "author", "cover_image_path", "media_item_id", "title", "author", "cover_image_path",
"has_conflict", "format_group", "badge", "has_conflict", "format_group", "badge",
"viewer_percentage", "library_name", "viewer_percentage", "library_name",
// Episode-collections extras (2.3 widening, every surface emits):
"is_episode", "feed_title", "download_state", "hidden",
} }
func collectionURL(setup *TestServerSetup, id string) string { func collectionURL(setup *TestServerSetup, id string) string {
@@ -500,6 +502,7 @@ func TestContractPin_CollectionsJSON(t *testing.T) {
"id", "user_id", "name", "description", "color", "icon", "id", "user_id", "name", "description", "color", "icon",
"auto_assign_rules", "view_settings", "books", "book_count", "auto_assign_rules", "view_settings", "books", "book_count",
"is_smart", "created_at", "up_next", "is_smart", "created_at", "up_next",
"rule_match_mode", // item-2.4 widening (docs/contract_pins.md)
} }
t.Run("user collection with library_id (badged branch)", func(t *testing.T) { t.Run("user collection with library_id (badged branch)", func(t *testing.T) {
+13
View File
@@ -468,3 +468,16 @@ never-queued episodes) is stamped from the `podcast_queue_seq` SEQUENCE
at enqueue time so claim order = enqueue order (the effective listening at enqueue time so claim order = enqueue order (the effective listening
order). Raw-row pins (TestContractPin_MediaListBothBranches → order). Raw-row pins (TestContractPin_MediaListBothBranches →
rawRowKeys) whitelist the key. Nothing removed, nothing renamed. rawRowKeys) whitelist the key. Nothing removed, nothing renamed.
## 2026-10-30 — episode collections (additive widening, plan item 2/2.3/2.4)
Two shapes widen, both ADDITIVE — nothing removed, nothing renamed:
- **Collections envelope** (`GET /api/collections/:id` → envelopeKeys)
gains `rule_match_mode` (smart-collection match semantics, 'any'
legacy default | 'all'). Static collections emit the default value.
- **BookInfo** (every card surface → bookInfoKeys) gains four STABLE
keys — `is_episode`, `feed_title`, `download_state`, `hidden` — the
episode-collections extras. Adapters that predate episode awareness
emit their zero values (`false`/`""`), matching the
viewer_percentage precedent (nil-representable, key always present).
+8 -5
View File
@@ -50,11 +50,14 @@ type BookInfo struct {
// LEFT JOIN missed under RLS (unsubscribed episode, or a // LEFT JOIN missed under RLS (unsubscribed episode, or a
// family-lens hidden book) — the row renders as an "unavailable" // family-lens hidden book) — the row renders as an "unavailable"
// hint instead of silently vanishing. Adapters that don't load // hint instead of silently vanishing. Adapters that don't load
// visibility leave Hidden false (cards stay fully rendered). // visibility leave Hidden false (cards stay fully rendered). The
IsEpisode bool `json:"is_episode,omitempty"` // four keys are NOT omitempty: every BookInfo surface emits the
FeedTitle string `json:"feed_title,omitempty"` // same stable key set (the viewer_percentage precedent —
DownloadState string `json:"download_state,omitempty"` // contract-pin widening, docs/contract_pins.md 2026-10-30).
Hidden bool `json:"hidden,omitempty"` IsEpisode bool `json:"is_episode"`
FeedTitle string `json:"feed_title"`
DownloadState string `json:"download_state"`
Hidden bool `json:"hidden"`
} }
// LibraryTypeMap builds libraryID→library-type-name for badge resolution // LibraryTypeMap builds libraryID→library-type-name for badge resolution