feat(collections): new podcast smart collections are BORN reversed
Per user request, same shape as0e29eff7— 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 from81e54f89corrected: the API envelope's match list is not chain-ordered and never was — order pins live on the SSR page and reader chain.
This commit is contained in:
1 parent
0e29eff7b7
commit
d56031f334
3 files changed
+106
-8
No files matched your search
@@ -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, `<option value="reverse" selected`,
|
||||
"the collection's dropdown shows the reversed default selected")
|
||||
|
||||
// aNew is feed position 1: FIRST under native, LAST under reverse.
|
||||
code, body = podcastAuthed(t, setup, regToken, "GET",
|
||||
@@ -2352,3 +2360,58 @@ func TestRulesPageEmbedsIdLabels(t *testing.T) {
|
||||
"a family-lens-hidden library is not named, even by explicit id")
|
||||
})
|
||||
}
|
||||
|
||||
// ---- creation seed: new podcast smart collections are BORN reversed ----
|
||||
|
||||
// TestSmartCollectionBornReversed pins the 2026-11-01 creation default:
|
||||
// an episodes-scope smart collection is created with
|
||||
// view_settings.chain_order = "reverse" (the dropdown's default
|
||||
// selection and the chain's behavior come from the STORED value — no
|
||||
// resolution-logic changes). Books-scope smart collections and statics
|
||||
// get nothing seeded; an explicit client value is never clobbered.
|
||||
func TestSmartCollectionBornReversed(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
regToken := setup.RegularToken
|
||||
|
||||
create := func(t *testing.T, payload string) string {
|
||||
code, body := podcastAuthed(t, setup, regToken, "POST", "/api/collections", payload)
|
||||
require.Equal(t, http.StatusCreated, code, "%s", string(body))
|
||||
var coll struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(body, &coll))
|
||||
t.Cleanup(func() {
|
||||
podcastAuthed(t, setup, regToken, "DELETE", "/api/collections/"+coll.ID, "")
|
||||
})
|
||||
return coll.ID
|
||||
}
|
||||
|
||||
t.Run("episodes_scope_seeded_reverse", func(t *testing.T) {
|
||||
id := create(t, `{"name":"test BornRev Ep `+t.Name()+`","filter_tree":{"scope":"episodes","mode":"all","children":[]}}`)
|
||||
_, body := podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||
assert.Contains(t, string(body), `"chain_order":"reverse"`,
|
||||
"a new episodes smart collection carries the reversed default")
|
||||
})
|
||||
|
||||
t.Run("explicit_view_settings_not_clobbered", func(t *testing.T) {
|
||||
id := create(t, `{"name":"test BornRev Keep `+t.Name()+`","filter_tree":{"scope":"episodes","mode":"all","children":[]},"view_settings":{"chain_order":"native"}}`)
|
||||
_, body := podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||
assert.Contains(t, string(body), `"chain_order":"native"`,
|
||||
"an explicit client chain_order is never clobbered by the seed")
|
||||
assert.NotContains(t, string(body), `"chain_order":"reverse"`)
|
||||
})
|
||||
|
||||
t.Run("books_scope_not_seeded", func(t *testing.T) {
|
||||
id := create(t, `{"name":"test BornRev Books `+t.Name()+`","filter_tree":{"scope":"books","mode":"all","children":[]}}`)
|
||||
_, body := podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||
assert.NotContains(t, string(body), `"chain_order"`,
|
||||
"books-scope smart collections get no chain_order seed")
|
||||
})
|
||||
|
||||
t.Run("static_not_seeded", func(t *testing.T) {
|
||||
id := create(t, `{"name":"test BornRev Static `+t.Name()+`"}`)
|
||||
_, body := podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||
assert.NotContains(t, string(body), `"chain_order"`,
|
||||
"static collections get no chain_order seed")
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user