feat(filter): label endpoint names id values for client-held trees

The SSR embed (65f257f3) covers trees the server renders; one path
still fell through to the flawed fallback — applying a SAVED filter
from a cold bookshelf. That page was rendered before any filter
existed (nothing embedded), and the fallback reused the suggest facet,
whose rows are MEDIA-CONSTRAINED: "feed in [A,B,C] AND title contains
Chapter" labeled only the feeds with a matching episode (the exact
live repro from the original bug report).

- GET /api/filter/labels?field=X&ids=a,b,c resolves names straight
  from the entity tables via the shared ResolveFieldLabels core
  (extracted from FilterTreeLabels): no media join, no tree
  constraint. RLS/visibility ride the request identity — a
  zero-episode subscribed feed resolves its title (pinned; the facet
  path structurally could not), an unsubscribed feed id stays unnamed
  (pinned), non-id fields are 400 (pinned), ids cap at 100.
- Client: resolveIdLabels/resolveLabelsAsync use ONLY the endpoint;
  the quiet suggest-fetch mode is deleted (its only caller was the
  flawed fallback). The saved-filter apply flow resolves through the
  endpoint and rebuilds the chips bar when labels land — chips render
  raw ids only while the round trip is in flight, never stuck.
- 188 web tests green; Go endpoint pins + filter/episode slices green;
  contract_pins documents the endpoint and the fallback removal.
This commit is contained in:
John O'Keefe committed 2026-11-01 13:40:25 -05:00
1 parent a20741ccc9
commit 96a40f7d88
8 files changed
+274 -78

No files matched your search

+66
View File
@@ -2415,3 +2415,69 @@ func TestSmartCollectionBornReversed(t *testing.T) {
"static collections get no chain_order seed")
})
}
// ---- label endpoint: names id-kind values for client-held trees ----
// TestFilterLabelsEndpoint pins GET /api/filter/labels (2026-11-01):
// the client path that never touches a server render — applying a
// SAVED filter from a cold bookshelf — resolves names through ENTITY
// tables, not the media-constrained suggest facet. A zero-episode feed
// resolves its title (the facet path structurally could not); an
// unsubscribed feed id resolves to nothing (RLS); a non-id field is a
// 400.
func TestFilterLabelsEndpoint(t *testing.T) {
setup := setupTestServer(t)
regUUID := getRegularUserID(t, setup.DB)
regToken := setup.RegularToken
lib, _ := createPodcastsLibrary(t, setup, "test LabelsEP Podcasts")
marker := fmt.Sprintf("labelsep%d", time.Now().UnixNano()%100000)
feedA, err := setup.DB.CreatePodcastFeed(adminSeedCtx(t, setup), database.CreatePodcastFeedParams{
LibraryID: lib.ID,
FeedUrl: "https://feed.invalid/" + marker + "-a.xml",
Title: "LabelsEP Feed A " + marker,
})
require.NoError(t, err)
feedB, err := setup.DB.CreatePodcastFeed(adminSeedCtx(t, setup), database.CreatePodcastFeedParams{
LibraryID: lib.ID,
FeedUrl: "https://feed.invalid/" + marker + "-b.xml",
Title: "LabelsEP Feed B " + marker,
})
require.NoError(t, err)
// feedA is subscribed; feedB is NOT (RLS must hide it even by id).
subscribeRaw(t, setup, regUUID, feedA.ID)
feedAID := uuid.UUID(feedA.ID.Bytes).String()
feedBID := uuid.UUID(feedB.ID.Bytes).String()
t.Run("zero_episode_feed_resolves", func(t *testing.T) {
code, body := podcastAuthed(t, setup, regToken, "GET",
"/api/filter/labels?field=feed&ids="+feedAID, "")
require.Equal(t, http.StatusOK, code, "%s", string(body))
assert.Contains(t, string(body), `"labels"`)
assert.Contains(t, string(body), "LabelsEP Feed A "+marker,
"a subscribed feed with ZERO episodes resolves its title (no media join)")
})
t.Run("comma_joined_ids_resolve_together", func(t *testing.T) {
code, body := podcastAuthed(t, setup, regToken, "GET",
"/api/filter/labels?field=feed&ids="+feedAID+","+feedBID, "")
require.Equal(t, http.StatusOK, code)
assert.Contains(t, string(body), "LabelsEP Feed A "+marker)
})
t.Run("unsubscribed_feed_stays_unnamed", func(t *testing.T) {
code, body := podcastAuthed(t, setup, regToken, "GET",
"/api/filter/labels?field=feed&ids="+feedBID, "")
require.Equal(t, http.StatusOK, code)
assert.NotContains(t, string(body), "LabelsEP Feed B "+marker,
"an unsubscribed feed id is invisible to resolution (RLS)")
})
t.Run("non_id_field_is_400", func(t *testing.T) {
code, body := podcastAuthed(t, setup, regToken, "GET",
"/api/filter/labels?field=title&ids=whatever", "")
assert.Equal(t, http.StatusBadRequest, code, "%s", string(body))
})
}