feat(docs): "?" help affordance on every filter mount + docs loader fixes

The filter manual (1ed1602e) was reachable only by knowing /docs
already existed — nothing in the UI pointed at it (the parked manual
plan's open decision). Every filter-builder mount now carries a "?"
in its header linking to the guide (opens in a new tab so drawer and
editor state survive): the All Books drawer and the collection rules
page — the two mounts sharing FilterBuilderRows. The docs home
indexes the manual (guide, cookbook, generated field reference).

Two docs-loader fixes surfaced by pinning this:
- Folder URLs resolve to their index page: /docs/user/filters now
  serves user/filters/index.md (the manual is a folder: guide +
  cookbook + generated reference); previously only root /docs had
  index handling.
- The docs folder path is resolved by walking upward (bounded):
  `go test` runs test binaries from the PACKAGE directory, so the
  relative "docs" path resolved to a folder that didn't exist there
  and every /docs route 404'd in tests while working in prod —
  found by the new pin, fixed so tests exercise the docs routes
  exactly as prod serves them.

Pins: TestFilterHelpAffordance (folder URL serves the guide; drawer
and rules pages carry the link). internal/docs suite + full suite
green (239s).
This commit is contained in:
John O'Keefe committed 2026-11-01 16:22:35 -05:00
1 parent 1ed1602e70
commit a4dc03984c
7 files changed
+118 -24

No files matched your search

+57
View File
@@ -2520,3 +2520,60 @@ func TestFilterLabelsEndpoint(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, code, "%s", string(body))
})
}
// ---- the "?" affordance: the builder links to its manual ----
// TestFilterHelpAffordance pins the help links (2026-11-01): every
// filter-builder mount carries a "?" linking to the manual, and the
// manual's FOLDER URL resolves to its index page (the docs loader's
// folder-index fallback — the guide is docs/user/filters/index.md).
func TestFilterHelpAffordance(t *testing.T) {
setup := setupTestServer(t)
regToken := setup.RegularToken
t.Run("docs_folder_url_serves_the_guide", func(t *testing.T) {
req, _ := http.NewRequest("GET", setup.Server.URL+"/docs/user/filters", nil)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "the folder URL resolves to index.md; body: %.400s", string(body))
assert.Contains(t, string(body), "Filters — the complete guide")
})
t.Run("drawer_carries_help_link", func(t *testing.T) {
req, _ := http.NewRequest("GET", setup.Server.URL+"/bookshelf", nil)
req.Header.Set("Authorization", "Bearer "+regToken)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
body, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Contains(t, string(body), `href="/docs/user/filters"`,
"the All Books drawer links to the filter guide")
})
t.Run("rules_page_carries_help_link", func(t *testing.T) {
marker := fmt.Sprintf("helpaff%d", time.Now().UnixNano()%100000)
code, body := podcastAuthed(t, setup, regToken, "POST", "/api/collections",
`{"name":"test HelpAff `+marker+`","filter_tree":{"scope":"episodes","mode":"all","children":[]}}`)
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, "")
})
req, _ := http.NewRequest("GET", setup.Server.URL+"/collections/"+coll.ID+"/rules", nil)
req.Header.Set("Authorization", "Bearer "+regToken)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
pageBody, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Contains(t, string(pageBody), `href="/docs/user/filters"`,
"the collection rules editor links to the filter guide")
})
}