Artifact 8f6e7bd4ef96b541ca8fd3c2d2e9fce93d70ab35caa6b84ac3f355ed2a96a0f9:
- File
internal/web/timelinelive_test.go
— part of check-in
[550f9601a2]
at
2026-08-25 22:02:26
on branch tl-filter
— Timeline stream: keep the reader's filter
The check-ins default lived only in the page handler, so a reader on the default timeline had an empty query string — and the live stream opened with it asked for no filter at all. The first tick after a landing replaced their check-ins with everything.
Both the page and the stream now resolve through timelineQuery. (user: dmestas, size: 4745) [annotate] [blame] [check-ins using] [more...]
package web import ( "net/url" "path/filepath" "strings" "testing" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/artifact" "craftdesign.group/quarry/internal/index" "craftdesign.group/quarry/internal/ops" ) // A reader on the default timeline sees check-ins. Their URL carries no // query at all to say so, and the live stream is opened with that same // empty query — so the resolution of "no filter" to "check-ins" has to // live somewhere both the page and the stream reach. It used to live in // the page handler alone, and the first stream tick after a landing // swapped their check-ins for everything. func TestTimelineQueryDefaultsToCheckins(t *testing.T) { vals, _ := timelineQuery(url.Values{}, "demo") if got := vals.Get("y"); got != "ci" { t.Fatalf("empty query resolved y=%q, want ci", got) } if got := vals.Get("repo"); got != "demo" { t.Fatalf("repo=%q, want demo", got) } } // The page and the stream must agree for every filter, not just the // default one — the stream re-renders what the page rendered. func TestTimelineQueryKeepsChosenFilter(t *testing.T) { for _, y := range []string{"all", "ci", "w", "t", "e", "f"} { vals, _ := timelineQuery(url.Values{"y": {y}}, "demo") if got := vals.Get("y"); got != y { t.Errorf("y=%q survived as %q", y, got) } } vals, _ := timelineQuery(url.Values{"branch": {"trunk"}, "tag": {"rel"}, "user": {"dm"}}, "demo") for k, want := range map[string]string{"branch": "trunk", "tag": "rel", "user": "dm"} { if got := vals.Get(k); got != want { t.Errorf("%s=%q, want %q", k, got, want) } } } // n= is the page's name for the op's limit, and the page size shown in // the Max control has to be the one actually asked for. Out-of-range // values fall back rather than reaching the op's validator. func TestTimelineQueryMapsPageSize(t *testing.T) { vals, limit := timelineQuery(url.Values{"n": {"100"}}, "demo") if vals.Get("limit") != "100" || limit != 100 { t.Fatalf("n=100 gave limit=%q/%d, want 100/100", vals.Get("limit"), limit) } if _, limit := timelineQuery(url.Values{}, "demo"); limit != 50 { t.Fatalf("default limit=%d, want 50", limit) } if _, limit := timelineQuery(url.Values{"n": {"9000"}}, "demo"); limit != 50 { t.Fatalf("out-of-range n gave limit=%d, want the 50 default", limit) } } // timelineQuery must not write through to its caller's values: the page // handler passes req.URL.Query() and then reads the raw query back for // the filter controls. func TestTimelineQueryDoesNotMutateInput(t *testing.T) { in := url.Values{"branch": {"trunk"}} timelineQuery(in, "demo") if in.Get("y") != "" || in.Get("repo") != "" { t.Fatalf("input mutated: %v", in) } } // The decisive one: the live stream must re-render the view the reader // is actually looking at. A reader on the default timeline sees // check-ins and has no query string saying so, so this drives the same // path the stream drives — timelineFragment with the empty query the // page handed it — and asserts a wiki edit does not appear in it. func TestTimelineFragmentHonoursTheDefaultView(t *testing.T) { ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) if err := ix.FoldNow([]index.Parsed{ {Hash: "c0de", Art: &artifact.Artifact{Type: artifact.Manifest, Comment: "MARKER-CHECKIN", Date: "2026-08-15T10:00:00", User: "dan"}}, {Hash: "w1k1", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "MARKER-WIKI", Date: "2026-08-15T11:00:00", User: "dan", Mimetype: "text/x-markdown", Body: []byte("body\n")}}, }); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "9.9.9-test", Repos: api.NewRepos(map[string]*api.Repo{"test": {Index: ix}})}); err != nil { t.Fatal(err) } // Empty query: exactly what the page passes when the reader has // touched no filter, which is the overwhelmingly common case. frag, err := timelineFragment(r, "test", url.Values{}) if err != nil { t.Fatal(err) } if !strings.Contains(frag, "MARKER-CHECKIN") { t.Errorf("default stream fragment dropped the check-in:\n%s", frag) } if strings.Contains(frag, "MARKER-WIKI") { t.Errorf("default stream fragment widened to everything — the wiki edit\n" + "leaked into a check-ins timeline, which is what a reader saw a\n" + "few seconds after any landing") } // And the widening is still available when it is asked for. all, err := timelineFragment(r, "test", url.Values{"y": {"all"}}) if err != nil { t.Fatal(err) } if !strings.Contains(all, "MARKER-WIKI") || !strings.Contains(all, "MARKER-CHECKIN") { t.Errorf("y=all did not carry both:\n%s", all) } }