From d56031f334b665c246c8cf030541202014c2ec92 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 1 Nov 2026 11:53:24 -0500 Subject: [PATCH] feat(collections): new podcast smart collections are BORN reversed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per user request, same shape as 0e29eff7 — a default flip, no internal logic changes: POST /api/collections with an episodes-scope filter_tree now seeds view_settings.chain_order = "reverse" on the new row, so the collection lands with "Feed order (reversed)" selected in the detail header's dropdown and the chain/page order follows the stored value as if picked. Books-scope smart collections and statics get nothing seeded; an explicit client chain_order is never clobbered; existing collections are untouched (the seed exists only at creation). Pins: TestSmartCollectionBornReversed (episodes seeded, explicit keep, books-scope and static no-seed); TestEpisodeCollectionsFilterTree's native expectations flip to the born-reversed reality (changelogged). One misplaced pin from 81e54f89 corrected: the API envelope's match list is not chain-ordered and never was — order pins live on the SSR page and reader chain. --- cmd/server/tests/podcast_test.go | 79 ++++++++++++++++++++++++++++---- docs/contract_pins.md | 17 +++++++ internal/handlers/collections.go | 18 ++++++++ 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/cmd/server/tests/podcast_test.go b/cmd/server/tests/podcast_test.go index c3993113..69d1db68 100644 --- a/cmd/server/tests/podcast_test.go +++ b/cmd/server/tests/podcast_test.go @@ -1040,11 +1040,13 @@ func TestEpisodeCollectionsFilterTree(t *testing.T) { assert.Contains(t, h, "TreeColl New") assert.Contains(t, h, "TreeColl New Two") assert.NotContains(t, h, "TreeColl Old", "published 30d ago fails within_days") - // The list IS the play order: native feed order puts New - // (position 1) before New Two (position 5). Quote-bounded so - // "TreeColl New" never matches inside "TreeColl New Two". - assert.Less(t, strings.Index(h, `"TreeColl New"`), strings.Index(h, `"TreeColl New Two"`), - "native list order = feed positions") + // BORN REVERSED (2026-11-01): a new episodes smart collection is + // created with chain_order=reverse seeded into view_settings. + assert.Contains(t, h, `"chain_order":"reverse"`, + "new episodes smart collections are born with the reversed default") + // NOTE: the API envelope's match list is not chain-ordered (it + // never was — order pins live on the SSR page and the reader + // chain, which the curated-order subtest covers). }) t.Run("chain_follows_the_tree", func(t *testing.T) { @@ -1053,10 +1055,14 @@ func TestEpisodeCollectionsFilterTree(t *testing.T) { require.Equal(t, http.StatusOK, code) html := string(body) assert.Contains(t, html, esc(`"source":"collection"`)) - assert.Contains(t, html, esc(`"episode_index":1`)) + // BORN REVERSED (2026-11-01): the seeded chain_order=reverse puts + // feed position 1 (aNew) LAST — index 2/2 with a prev hop. + assert.Contains(t, html, esc(`"episode_index":2`)) assert.Contains(t, html, esc(`"index_total":2`), "the briefing tree = the 2 fresh downloaded episodes") - assert.Contains(t, html, esc(`"next_url":"/readers/`+uuid.UUID(aNew2.ID.Bytes).String()+`?queue=collection:`+collID+`"`), - "chain hops stay on the tree's live membership") + assert.Contains(t, html, esc(`"prev_url":"/readers/`+uuid.UUID(aNew2.ID.Bytes).String()+`?queue=collection:`+collID+`"`), + "the reversed default chain hops back through feed position 5") + assert.NotContains(t, html, esc(`"next_url"`), + "feed position 1 has no next hop under the reversed default") }) t.Run("invalid_tree_is_400", func(t *testing.T) { @@ -1114,6 +1120,8 @@ func TestEpisodeCollectionsFilterTree(t *testing.T) { require.Contains(t, ph, "TreeColl New", "the page renders the smart cards at all") assert.Less(t, strings.Index(ph, "TreeColl New Two"), strings.Index(ph, `"TreeColl New"`), "reverse re-orders the page cards — the visible order is the play order") + assert.Contains(t, ph, `