feat(collections): the rules-Apply path seeds the reversed default too
The creation seed (d56031f3) covered born-smart collections; a STATIC
collection that becomes smart through the rules editor's Apply landed
without any curated order — its dropdown stayed "Feed order" while
every other podcast smart collection opened reversed. Same decision,
same guardrails (2026-11-01 user approved closing this gap): a PUT
carrying an episodes-scope tree seeds chain_order=reverse only when
neither the request NOR the stored view_settings names one — a
deliberate native (client-sent or saved) is never clobbered, and
books-scope trees seed nothing.
Pins: apply-seeds-when-unordered, respects-stored-native,
respects-client-native (TestSmartCollectionBornReversed now covers
both seed paths); changelogged in contract_pins.
This commit is contained in:
1 parent
96a40f7d88
commit
57bc0e9dd4
3 files changed
+69
No files matched your search
@@ -2414,6 +2414,43 @@ func TestSmartCollectionBornReversed(t *testing.T) {
|
|||||||
assert.NotContains(t, string(body), `"chain_order"`,
|
assert.NotContains(t, string(body), `"chain_order"`,
|
||||||
"static collections get no chain_order seed")
|
"static collections get no chain_order seed")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The creation seed's twin (2026-11-01): APPLYING an episodes tree
|
||||||
|
// to a collection with no curated order seeds reverse — a static
|
||||||
|
// collection becoming smart lands with the same default a
|
||||||
|
// born-smart one gets. Explicit values (client-sent or stored) are
|
||||||
|
// never clobbered.
|
||||||
|
t.Run("apply_path_seeds_when_unordered", func(t *testing.T) {
|
||||||
|
id := create(t, `{"name":"test BornRev Apply `+t.Name()+`"}`)
|
||||||
|
code, body := podcastAuthed(t, setup, regToken, "PUT", "/api/collections/"+id,
|
||||||
|
`{"name":"test BornRev Apply `+t.Name()+`","filter_tree":{"scope":"episodes","mode":"all","children":[]}}`)
|
||||||
|
require.Equal(t, http.StatusCreated, code, "%s", string(body))
|
||||||
|
_, body = podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||||
|
assert.Contains(t, string(body), `"chain_order":"reverse"`,
|
||||||
|
"applying an episodes tree to an unordered collection seeds reverse")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("apply_path_respects_stored_native", func(t *testing.T) {
|
||||||
|
id := create(t, `{"name":"test BornRev KeepNative `+t.Name()+`","view_settings":{"chain_order":"native"}}`)
|
||||||
|
code, body := podcastAuthed(t, setup, regToken, "PUT", "/api/collections/"+id,
|
||||||
|
`{"name":"test BornRev KeepNative `+t.Name()+`","filter_tree":{"scope":"episodes","mode":"all","children":[]}}`)
|
||||||
|
require.Equal(t, http.StatusCreated, code, "%s", string(body))
|
||||||
|
_, body = podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||||
|
assert.Contains(t, string(body), `"chain_order":"native"`,
|
||||||
|
"a stored explicit chain_order is never clobbered by the apply seed")
|
||||||
|
assert.NotContains(t, string(body), `"chain_order":"reverse"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("apply_path_respects_client_native", func(t *testing.T) {
|
||||||
|
id := create(t, `{"name":"test BornRev ClientNative `+t.Name()+`"}`)
|
||||||
|
code, body := podcastAuthed(t, setup, regToken, "PUT", "/api/collections/"+id,
|
||||||
|
`{"name":"test BornRev ClientNative `+t.Name()+`","filter_tree":{"scope":"episodes","mode":"all","children":[]},"view_settings":{"chain_order":"native"}}`)
|
||||||
|
require.Equal(t, http.StatusCreated, code, "%s", string(body))
|
||||||
|
_, body = podcastAuthed(t, setup, regToken, "GET", "/api/collections/"+id, "")
|
||||||
|
assert.Contains(t, string(body), `"chain_order":"native"`,
|
||||||
|
"the client's explicit chain_order wins over the apply seed")
|
||||||
|
assert.NotContains(t, string(body), `"chain_order":"reverse"`)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- label endpoint: names id-kind values for client-held trees ----
|
// ---- label endpoint: names id-kind values for client-held trees ----
|
||||||
|
|||||||
@@ -703,3 +703,14 @@ resolves through the endpoint and rebuilds the chips bar when labels
|
|||||||
land. Pinned (TestFilterLabelsEndpoint): a zero-episode feed resolves
|
land. Pinned (TestFilterLabelsEndpoint): a zero-episode feed resolves
|
||||||
its title, comma-joined ids resolve together, an unsubscribed feed id
|
its title, comma-joined ids resolve together, an unsubscribed feed id
|
||||||
stays unnamed (RLS), non-id fields are 400.
|
stays unnamed (RLS), non-id fields are 400.
|
||||||
|
|
||||||
|
## 2026-11-01 — the rules-Apply path seeds the reversed default too
|
||||||
|
|
||||||
|
PUT /api/collections/:id carrying an EPISODES-scope filter_tree seeds
|
||||||
|
`chain_order = "reverse"` when neither the request nor the stored
|
||||||
|
view_settings names one — the creation seed's twin: a static collection
|
||||||
|
becoming smart lands with the same default a born-smart one gets.
|
||||||
|
Explicit values (client-sent or stored — e.g. a deliberate native)
|
||||||
|
are never clobbered. Pinned (TestSmartCollectionBornReversed):
|
||||||
|
apply-seeds-when-unordered, respects stored native, respects client
|
||||||
|
native.
|
||||||
@@ -607,6 +607,27 @@ func (h *CollectionHandler) UpdateCollection(c *echo.Context) error {
|
|||||||
if treeErr != nil {
|
if treeErr != nil {
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": treeErr.Error()})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": treeErr.Error()})
|
||||||
}
|
}
|
||||||
|
// Applying a tree to a collection with NO curated listening order
|
||||||
|
// seeds the reversed default (2026-11-01 — the creation seed's twin):
|
||||||
|
// a static collection becoming smart lands with the same default a
|
||||||
|
// born-smart one gets. Triple-guarded: the client's explicit
|
||||||
|
// chain_order wins, a STORED chain_order is never clobbered, and
|
||||||
|
// only episodes-scope trees seed (books have no episode chain).
|
||||||
|
if treeJSON != nil && (req.ViewSettings == nil || req.ViewSettings["chain_order"] == nil) {
|
||||||
|
if t, perr := services.ParseFilterTree(string(treeJSON)); perr == nil &&
|
||||||
|
t.FilterScope() == services.ScopeEpisodes {
|
||||||
|
if existing, gerr := h.collectionService.GetCollection(c.Request().Context(), collectionID); gerr == nil {
|
||||||
|
var stored map[string]interface{}
|
||||||
|
_ = json.Unmarshal(existing.ViewSettings, &stored)
|
||||||
|
if stored == nil || stored["chain_order"] == nil {
|
||||||
|
if req.ViewSettings == nil {
|
||||||
|
req.ViewSettings = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
req.ViewSettings["chain_order"] = "reverse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
collection, err := h.collectionService.UpdateCollection(
|
collection, err := h.collectionService.UpdateCollection(
|
||||||
c.Request().Context(),
|
c.Request().Context(),
|
||||||
collectionID,
|
collectionID,
|
||||||
|
|||||||
Reference in new issue
Block a user