Artifact ef5116f49ee78f8159a5b455b77eec664cc532b9a9c34f39e7ad504a695e1a9d:
- File
internal/api/register_test.go
— part of check-in
[ff7f5d3d69]
at
2026-08-23 22:03:24
on branch trunk
— check-in page: tag history, branch diff, manifest view
Fossil lets an author correct a check-in comment after the fact; quarry showed the superseded C card as if it were current, on 697 fossil check-ins. The correction is now applied at read, batched in AttachCI, with the original kept beside it — the stored card is an artifact fact and stays untouched. hidden and closed surface too.
Branch diff is the branch against its fork point, which is not the landing page's base (the target's current tip). Offered only where a fork point exists.
The manifest view renders the eight cards that are not the file list and counts the other 980, linking the tree.
Also: unified/side-by-side now swaps in place via datastar, keeping the window and correcting the URL; the wiki note panel lets blocks use the full width while prose keeps its measure. (user: dmestas, size: 55423) [annotate] [blame] [check-ins using]
package api import ( "context" "errors" "fmt" "path/filepath" "slices" "strings" "sync" "testing" "time" "craftdesign.group/quarry/internal/zitcli" "craftdesign.group/quarry/internal/artifact" "craftdesign.group/quarry/internal/chat" "craftdesign.group/quarry/internal/index" "craftdesign.group/quarry/internal/ops" ) func TestRegisterAllAndVersionOp(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "1.2.3-test"}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "meta.version", versionIn{}) if err != nil { t.Fatal(err) } if got.(versionOut).Version != "1.2.3-test" { t.Fatalf("got %+v", got) } } func TestRegisteredOpsAppearInSpec(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v"}); err != nil { t.Fatal(err) } spec, err := ops.Spec(r, "quarry", "v") if err != nil { t.Fatal(err) } if spec.Paths["/api/v1/version"].Get == nil { t.Fatal("meta.version missing from generated spec") } } func wikiIndex(t *testing.T) *index.Index { t.Helper() ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) batch := []index.Parsed{ {Hash: "aaa1", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Design", Date: "2026-08-15T10:00:00", User: "dan", Mimetype: "text/x-markdown", Body: []byte("# Design\n\ndetails\n")}}, {Hash: "aaa2", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Home", Date: "2026-08-15T10:00:01", User: "dan", Mimetype: "text/x-markdown", Body: []byte("See [[Design]] and [[Missing]].\n")}}, {Hash: "aaa3", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Home", Date: "2026-08-15T11:00:00", User: "eve", Mimetype: "text/x-markdown", Body: []byte("Revised. See [[Design]].\n")}}, } if err := ix.FoldNow(batch); err != nil { t.Fatal(err) } return ix } func TestWikiOps(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "wiki.list", wikiListIn{Repo: "test"}) if err != nil { t.Fatal(err) } list := got.(wikiListOut) if list.Count != 2 || list.Pages[0].Title != "Design" || list.Pages[1].Title != "Home" { t.Fatalf("list = %+v", list) } if list.Pages[1].User != "eve" { t.Fatalf("latest edit not surfaced: %+v", list.Pages[1]) } got, err = r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Home"}) if err != nil { t.Fatal(err) } page := got.(wikiGetOut) if !strings.Contains(page.HTML, `<a href="/r/test/wiki/Design">Design</a>`) { t.Fatalf("existing page not resolved:\n%s", page.HTML) } if !strings.Contains(page.Source, "Revised") { t.Fatalf("stale edit served: %+v", page) } if _, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Missing"}); err == nil { t.Fatal("want error for missing page") } } func TestWikiHistoryAndRevision(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "wiki.history", wikiHistoryIn{Repo: "test", Name: "Home"}) if err != nil { t.Fatal(err) } h := got.(wikiHistoryOut) if h.Count != 2 || h.Edits[0].Hash != "aaa3" || h.Edits[1].Hash != "aaa2" { t.Fatalf("history = %+v", h) } if h.Edits[0].User != "eve" || h.Edits[1].User != "dan" { t.Fatalf("edit users = %+v", h.Edits) } // An old revision serves its own content, not the fold. got, err = r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Home", Rev: "aaa2"}) if err != nil { t.Fatal(err) } page := got.(wikiGetOut) if page.Hash != "aaa2" || !strings.Contains(page.Source, "Missing") || strings.Contains(page.Source, "Revised") { t.Fatalf("revision fetch = %+v", page) } if _, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Home", Rev: "ffff"}); err == nil { t.Fatal("want error for unknown revision") } // "aaa" prefixes both edits of Home — ambiguous resolves to nothing. if _, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Home", Rev: "aaa"}); err == nil { t.Fatal("want error for ambiguous revision prefix") } if _, err := r.Invoke(context.Background(), "wiki.history", wikiHistoryIn{Repo: "test", Name: "Missing"}); err == nil { t.Fatal("want error for missing page history") } } func TestWikiGetUnresolvedRefStaysText(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } // The older Home revision links [[Missing]]; current revision does // not — fetch Design's referrer instead via a fresh page write. got, err := r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Design"}) if err != nil { t.Fatal(err) } if strings.Contains(got.(wikiGetOut).HTML, "/r/test/wiki/Missing") { t.Fatal("nonexistent page linked") } } func TestCheckinGetAndHashResolution(t *testing.T) { ix := wikiIndex(t) h1 := strings.Repeat("ab", 32) h2 := "abcd" + strings.Repeat("ef", 30) if err := ix.FoldNow([]index.Parsed{ {Hash: h1, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T09:00:00", User: "dan", Comment: "first"}}, {Hash: h2, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T09:30:00", User: "dan", Comment: "second", Parents: []string{h1}}}, {Hash: "bbb1", Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Refs", Date: "2026-08-15T12:00:00", User: "dan", Mimetype: "text/x-markdown", Body: []byte("Fixed in " + h2[:12] + ". Ambiguous: ab.\n")}}, }); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } // Prefix lookup: unique resolves, ambiguous does not. got, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: h2[:12]}) if err != nil { t.Fatal(err) } ci := got.(checkinGetOut) if ci.Hash != h2 || len(ci.Parents) != 1 || ci.Parents[0] != h1 { t.Fatalf("checkin = %+v", ci) } if _, err := r.Invoke(context.Background(), "checkin.get", checkinGetIn{Repo: "test", Hash: "abcdzz"}); err == nil { t.Fatal("want error for unknown hash") } // Wiki body hash references resolve through the same lookup. got, err = r.Invoke(context.Background(), "wiki.get", wikiGetIn{Repo: "test", Name: "Refs"}) if err != nil { t.Fatal(err) } html := got.(wikiGetOut).HTML // The resolved link carries the check-in's comment: hover title on // the anchor, visible excerpt beside it (GitHub's referenced-commit // rendering). if !strings.Contains(html, `<a href="/r/test/checkin/`+h2+`" title="second">`+h2[:12]+`</a>`) || !strings.Contains(html, `<span class="cref"> (second)</span>`) { t.Fatalf("hash ref not resolved with description:\n%s", html) } } func TestTicketOps(t *testing.T) { ix := wikiIndex(t) tid := strings.Repeat("cd", 20) if err := ix.FoldNow([]index.Parsed{ {Hash: "ccc1", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, Date: "2026-08-15T09:00:00", User: "dan", Fields: []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: "crash on empty input"}}}}, {Hash: "ccc2", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, Date: "2026-08-15T10:00:00", User: "eve", Fields: []artifact.Field{{Name: "resolution", Value: "Fixed"}, {Name: "status", Value: "Closed"}}}}, }); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } // The default listing is OPEN tickets, so a closed one is absent // until asked for. That is the whole point of the default: across // this corpus most of a tracker is finished work. got, err := r.Invoke(context.Background(), "ticket.list", ticketListIn{Repo: "test"}) if err != nil { t.Fatal(err) } if list := got.(ticketListOut); list.Count != 0 { t.Fatalf("a closed ticket appeared in the open listing: %+v", list) } got, err = r.Invoke(context.Background(), "ticket.list", ticketListIn{Repo: "test", State: "all"}) if err != nil { t.Fatal(err) } list := got.(ticketListOut) if list.Count != 1 || list.Tickets[0].Status != "Closed" || list.Tickets[0].Title != "crash on empty input" { t.Fatalf("list = %+v", list) } got, err = r.Invoke(context.Background(), "ticket.get", ticketGetIn{Repo: "test", ID: tid[:8]}) if err != nil { t.Fatal(err) } tk := got.(ticketGetOut).Ticket if tk.ID != tid || len(tk.Changes) != 2 { t.Fatalf("ticket = %+v", tk) } if fieldValue(tk.Fields, "status") != "Closed" || fieldValue(tk.Fields, "resolution") != "Fixed" { t.Fatalf("fold wrong: %+v", tk.Fields) } if tk.Created != "2026-08-15T09:00:00" || tk.Updated != "2026-08-15T10:00:00" { t.Fatalf("created/updated: %+v", tk) } } func TestSearchOp(t *testing.T) { ix := wikiIndex(t) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "search.query", searchIn{Repo: "test", Q: "Design", Limit: 50}) if err != nil { t.Fatal(err) } res := got.(searchOut) if res.Count == 0 { t.Fatalf("no hits: %+v", res) } } func TestEntityNotFoundWrapsSentinel(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } for name, in := range map[string]any{ "wiki.get": wikiGetIn{Repo: "test", Name: "NoSuchPage"}, "ticket.get": ticketGetIn{Repo: "test", ID: "deadbeef"}, "checkin.get": checkinGetIn{Repo: "test", Hash: "deadbeefdeadbeef"}, "checkin.diff": diffGetIn{Repo: "test", Hash: "deadbeefdeadbeef"}, } { _, err := r.Invoke(context.Background(), name, in) if !errors.Is(err, ops.ErrNotFound) { t.Errorf("%s: err %v does not wrap ErrNotFound", name, err) } } } func TestSearchTicketDocIsFoldedState(t *testing.T) { // tickets 050 + 130: search over tickets sees CURRENT state with a // human label; stale early-change values don't match at all. ix := wikiIndex(t) tid := strings.Repeat("ef", 20) if err := ix.FoldNow([]index.Parsed{ {Hash: "ddd1", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, Date: "2026-08-15T09:00:00", User: "dan", Fields: []artifact.Field{{Name: "status", Value: "Openish"}, {Name: "title", Value: "typo in docs"}}}}, {Hash: "ddd2", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, Date: "2026-08-15T10:00:00", User: "eve", Fields: []artifact.Field{{Name: "status", Value: "Closedish"}}}}, }); err != nil { t.Fatal(err) } hits, err := ix.Search("typo", "", 10) if err != nil { t.Fatal(err) } if len(hits) != 1 || hits[0].Label != "typo in docs" { t.Fatalf("hits = %+v", hits) } if !strings.Contains(hits[0].Snippet, "⟦typo⟧") { t.Fatalf("marker missing: %q", hits[0].Snippet) } // The superseded status value is gone from the corpus entirely. if stale, _ := ix.Search("Openish", "", 10); len(stale) != 0 { t.Fatalf("stale field value still searchable: %+v", stale) } if cur, _ := ix.Search("Closedish", "", 10); len(cur) != 1 { t.Fatalf("current field value not searchable: %+v", cur) } } func TestSearchLimitZeroIsLiteral(t *testing.T) { // ticket 080: an explicit limit=0 caps to zero results. r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "search.query", searchIn{Repo: "test", Q: "Design", Limit: 0}) if err != nil { t.Fatal(err) } if got.(searchOut).Count != 0 { t.Fatalf("limit=0 returned results: %+v", got) } } // mustRepo fetches a registered repo in a test, failing loudly rather // than nil-panicking three lines later. func mustRepo(t *testing.T, rs *Repos, name string) *Repo { t.Helper() r, ok := rs.Get(name) if !ok { t.Fatalf("no repo %q registered", name) } return r } func testRepos(ix *index.Index) *Repos { return NewRepos(map[string]*Repo{"test": {Index: ix}}) } func TestUnknownRepoIsNotFound(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } _, err := r.Invoke(context.Background(), "wiki.list", wikiListIn{Repo: "nope"}) if !errors.Is(err, ops.ErrNotFound) { t.Fatalf("unknown repo: err %v does not wrap ErrNotFound", err) } } func TestRepoList(t *testing.T) { ix := wikiIndex(t) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{ "beta": {Index: ix}, "alpha": {Index: ix}, })}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "repo.list", repoListIn{}) if err != nil { t.Fatal(err) } fl := got.(repoListOut) if fl.Count != 2 || fl.Repos[0].Name != "alpha" || fl.Repos[1].Name != "beta" { t.Fatalf("fleet = %+v", fl) } } func TestTimelinePagination(t *testing.T) { ix := wikiIndex(t) var hashes []string var batch []index.Parsed for i := 0; i < 5; i++ { h := strings.Repeat(fmt.Sprintf("%02d", i), 32) hashes = append(hashes, h) batch = append(batch, index.Parsed{Hash: h, Art: &artifact.Artifact{ Type: artifact.Manifest, User: "dan", Date: fmt.Sprintf("2026-08-15T0%d:00:00", i), Comment: fmt.Sprintf("commit %d", i), }}) } if err := ix.FoldNow(batch); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 2, Y: "ci"}) if err != nil { t.Fatal(err) } p1 := got.(timelineOut) if p1.Count != 2 || p1.Entries[0].Comment != "commit 4" || p1.Next == "" { t.Fatalf("page 1 = %+v", p1) } got, err = r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 2, Before: p1.Next, Y: "ci"}) if err != nil { t.Fatal(err) } p2 := got.(timelineOut) if p2.Count != 2 || p2.Entries[0].Comment != "commit 2" || p2.Next == "" { t.Fatalf("page 2 = %+v", p2) } got, err = r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 2, Before: p2.Next, Y: "ci"}) if err != nil { t.Fatal(err) } p3 := got.(timelineOut) if p3.Count != 1 || p3.Entries[0].Comment != "commit 0" || p3.Next != "" { t.Fatalf("page 3 = %+v", p3) } } func branchArts(h1, h2, h3 string) []index.Parsed { // root (tagged trunk) ← child (untagged) ← branch start (tagged dev) return []index.Parsed{ {Hash: h1, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T09:00:00", User: "dan", Comment: "root", Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "trunk"}}}}, {Hash: h2, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T09:10:00", User: "dan", Comment: "on trunk", Parents: []string{h1}}}, {Hash: h3, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T09:20:00", User: "dan", Comment: "start dev", Parents: []string{h2}, Tags: []artifact.Tag{{Name: "*branch", Target: "*", Value: "dev"}}}}, } } func TestBranchPropagation(t *testing.T) { h1, h2, h3 := strings.Repeat("11", 32), strings.Repeat("22", 32), strings.Repeat("33", 32) arts := branchArts(h1, h2, h3) // Fold twice: forward order, and children-before-parents. The // wholesale recompute must converge identically (order independence // is what keeps the branch column disposability-safe). for name, order := range map[string][]int{"forward": {0, 1, 2}, "reversed": {2, 1, 0}} { ix, err := index.Open(filepath.Join(t.TempDir(), "i.db")) if err != nil { t.Fatal(err) } for _, i := range order { if err := ix.FoldNow(arts[i : i+1]); err != nil { t.Fatal(err) } } tl, _, err := ix.Timeline(index.TimelineFilter{Limit: 10, Kind: "ci"}) if err != nil { t.Fatal(err) } got := map[string]string{} for _, e := range tl { got[e.Comment] = e.Branch } want := map[string]string{"root": "trunk", "on trunk": "trunk", "start dev": "dev"} for c, b := range want { if got[c] != b { t.Errorf("%s order: %q on branch %q, want %q", name, c, got[c], b) } } ix.Close() } } func TestBranchFilterAndControlRetag(t *testing.T) { h1, h2, h3 := strings.Repeat("44", 32), strings.Repeat("55", 32), strings.Repeat("66", 32) ix, err := index.Open(filepath.Join(t.TempDir(), "i.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) if err := ix.FoldNow(branchArts(h1, h2, h3)); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 10, Branch: "dev"}) if err != nil { t.Fatal(err) } tl := got.(timelineOut) if tl.Count != 1 || tl.Entries[0].Comment != "start dev" { t.Fatalf("dev filter = %+v", tl) } // A later control artifact moves h3 to another branch; the recompute // must pick it up. (Tag priority between conflicting sources is // future work; a single retag must work.) if err := ix.FoldNow([]index.Parsed{{Hash: "ctl1", Art: &artifact.Artifact{ Type: artifact.Control, Date: "2026-08-15T10:00:00", User: "dan", Tags: []artifact.Tag{{Name: "*branch", Target: h3, Value: "renamed"}}, }}}); err != nil { t.Fatal(err) } got, err = r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 10, Branch: "renamed"}) if err != nil { t.Fatal(err) } if got.(timelineOut).Count != 1 { t.Fatalf("control retag not applied: %+v", got) } } func TestUnifiedTimeline(t *testing.T) { // wikiIndex has 3 wiki edits; add a check-in and a ticket change, // then assert the kinds interleave by date and each filter isolates. ix := wikiIndex(t) ci := strings.Repeat("77", 32) tid := strings.Repeat("88", 20) if err := ix.FoldNow([]index.Parsed{ {Hash: ci, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T10:30:00", User: "dan", Comment: "between the wiki edits"}}, {Hash: "tk1", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, Date: "2026-08-15T12:00:00", User: "eve", Fields: []artifact.Field{{Name: "title", Value: "a ticket"}, {Name: "status", Value: "Open"}}}}, }); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 50}) if err != nil { t.Fatal(err) } all := got.(timelineOut) // 3 wiki + 1 ci + 1 ticket = 5, newest first: ticket, wiki, ci, wiki, wiki kinds := []string{} for _, e := range all.Entries { kinds = append(kinds, e.Kind) } want := []string{"t", "w", "ci", "w", "w"} if fmt.Sprint(kinds) != fmt.Sprint(want) { t.Fatalf("kinds = %v, want %v", kinds, want) } // Ticket row carries the folded title and its id as ref. if e := all.Entries[0]; e.Ref != tid || !strings.Contains(e.Comment, "a ticket") { t.Fatalf("ticket event = %+v", e) } // Wiki row refs its title. if e := all.Entries[1]; e.Ref != "Home" || !strings.Contains(e.Comment, "Home") { t.Fatalf("wiki event = %+v", e) } for y, wantN := range map[string]int{"ci": 1, "w": 3, "t": 1} { got, err := r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 50, Y: y}) if err != nil { t.Fatal(err) } if n := got.(timelineOut).Count; n != wantN { t.Fatalf("y=%s: count %d, want %d", y, n, wantN) } } } // TestTicketListPagination walks the composite updated~ticket cursor // across three pages and checks global order and completeness. func TestTicketListPagination(t *testing.T) { ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) var batch []index.Parsed for i := 0; i < 7; i++ { tid := strings.Repeat(fmt.Sprintf("%02d", i), 20) batch = append(batch, index.Parsed{ Hash: fmt.Sprintf("h%02d", i), Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: tid, // Two tickets share an updated stamp to exercise the id tiebreak. Date: fmt.Sprintf("2026-08-15T%02d:00:00", 10+i/2), User: "dan", Fields: []artifact.Field{{Name: "title", Value: fmt.Sprintf("t%d", i)}}}, }) } if err := ix.FoldNow(batch); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } var seen []string before := "" pages := 0 for { got, err := r.Invoke(context.Background(), "ticket.list", ticketListIn{Repo: "test", Limit: 3, Before: before}) if err != nil { t.Fatal(err) } out := got.(ticketListOut) if len(out.Tickets) == 0 { t.Fatalf("empty page %d", pages) } for _, tk := range out.Tickets { seen = append(seen, tk.ID) } pages++ if out.Next == "" { break } before = out.Next } if pages != 3 || len(seen) != 7 { t.Fatalf("pages=%d seen=%d %v", pages, len(seen), seen) } uniq := map[string]bool{} for _, id := range seen { if uniq[id] { t.Fatalf("duplicate across pages: %s", id) } uniq[id] = true } } func forumTechIndex(t *testing.T) *index.Index { t.Helper() ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) root := strings.Repeat("f0", 20) reply := strings.Repeat("f1", 20) ev := strings.Repeat("e0", 20) if err := ix.FoldNow([]index.Parsed{ {Hash: root, Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: "Design question", Date: "2026-08-16T09:00:00", User: "dan", Body: []byte("How should we shape the [[Design]] doc?\n")}}, {Hash: reply, Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: root, InReplyTo: root, Date: "2026-08-16T10:00:00", User: "eve", Body: []byte("Start from the sync RFC.\n")}}, {Hash: strings.Repeat("e0a", 14)[:40], Art: &artifact.Artifact{Type: artifact.Technote, EventID: ev, EventTime: "2026-08-16T08:00:00", Date: "2026-08-16T08:00:00", User: "dan", Mimetype: "text/x-markdown", Comment: "Release notes", Body: []byte("# 0.2\n\nshipped the read plane\n")}}, }); err != nil { t.Fatal(err) } return ix } func TestForumOps(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(forumTechIndex(t))}); err != nil { t.Fatal(err) } root := strings.Repeat("f0", 20) got, err := r.Invoke(context.Background(), "forum.list", forumListIn{Repo: "test"}) if err != nil { t.Fatal(err) } list := got.(forumListOut) if list.Count != 1 || list.Threads[0].Title != "Design question" || list.Threads[0].Posts != 2 { t.Fatalf("forum list = %+v", list) } got, err = r.Invoke(context.Background(), "forum.get", forumGetIn{Repo: "test", Root: root[:12]}) if err != nil { t.Fatal(err) } th := got.(forumGetOut) if len(th.Posts) != 2 || th.Title != "Design question" { t.Fatalf("thread = %+v", th) } // Oldest first, reply carries its parent, and the opener's [[Design]] does not resolve (no such page) but renders as text. if !strings.Contains(th.Posts[0].HTML, "Design") || th.Posts[1].Parent != root { t.Fatalf("thread posts = %+v", th.Posts) } if _, err := r.Invoke(context.Background(), "forum.get", forumGetIn{Repo: "test", Root: "9999"}); err == nil { t.Fatal("want error for unknown thread") } } func TestTechnoteOps(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(forumTechIndex(t))}); err != nil { t.Fatal(err) } ev := strings.Repeat("e0", 20) got, err := r.Invoke(context.Background(), "technote.list", technoteListIn{Repo: "test"}) if err != nil { t.Fatal(err) } if l := got.(technoteListOut); l.Count != 1 || l.Notes[0].Event != ev { t.Fatalf("technote list = %+v", l) } got, err = r.Invoke(context.Background(), "technote.get", technoteGetIn{Repo: "test", Event: ev[:10]}) if err != nil { t.Fatal(err) } n := got.(technoteGetOut) if !strings.Contains(n.HTML, "shipped the read plane") || n.User != "dan" { t.Fatalf("technote = %+v", n) } if _, err := r.Invoke(context.Background(), "technote.get", technoteGetIn{Repo: "test", Event: "zzzz"}); err == nil { t.Fatal("want error for unknown technote") } } // Forum posts and technotes appear on the unified timeline and filter by kind. func TestForumTechnoteInTimeline(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(forumTechIndex(t))}); err != nil { t.Fatal(err) } got, err := r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 50, Y: "all"}) if err != nil { t.Fatal(err) } kinds := map[string]int{} for _, e := range got.(timelineOut).Entries { kinds[e.Kind]++ } if kinds["f"] != 2 || kinds["e"] != 1 { t.Fatalf("timeline kinds = %+v", kinds) } got, _ = r.Invoke(context.Background(), "timeline.list", timelineIn{Repo: "test", Limit: 50, Y: "f"}) for _, e := range got.(timelineOut).Entries { if e.Kind != "f" { t.Fatalf("y=f leaked kind %q", e.Kind) } } } type fakeChat struct{ msgs []chat.Message } func (f *fakeChat) Post(code string, m chat.Message) error { f.msgs = append(f.msgs, m) return nil } func (f *fakeChat) History(code string, limit int) ([]chat.Message, error) { return f.msgs, nil } func TestChatOps(t *testing.T) { fc := &fakeChat{} r := ops.NewRegistry() repos := testRepos(wikiIndex(t)) mustRepo(t, repos, "test").Code = "codeXYZ" if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Chat: fc}); err != nil { t.Fatal(err) } if _, err := r.Invoke(context.Background(), "chat.post", chatPostIn{Repo: "test", Text: "hello chat"}); err != nil { t.Fatal(err) } if len(fc.msgs) != 1 || fc.msgs[0].Text != "hello chat" || fc.msgs[0].User != "anonymous" { t.Fatalf("post = %+v", fc.msgs) } got, err := r.Invoke(context.Background(), "chat.history", chatHistoryIn{Repo: "test"}) if err != nil { t.Fatal(err) } if h := got.(chatHistoryOut); h.Count != 1 || h.Messages[0].Text != "hello chat" { t.Fatalf("history = %+v", h) } // Empty text is rejected by validation. if _, err := r.Invoke(context.Background(), "chat.post", chatPostIn{Repo: "test", Text: ""}); err == nil { t.Skip("validation runs in the router layer, not Invoke") } } // chat ops are absent when no chat plane is configured. func TestChatOpsAbsentWithoutPlane(t *testing.T) { r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil { t.Fatal(err) } if _, err := r.Invoke(context.Background(), "chat.post", chatPostIn{Repo: "test", Text: "x"}); err == nil { t.Fatal("chat.post should be unregistered without a chat plane") } } type fakeNotify struct { mu sync.Mutex events []NotifyEvent } func (f *fakeNotify) Trigger(_ context.Context, e NotifyEvent) error { f.mu.Lock() defer f.mu.Unlock() f.events = append(f.events, e) return nil } func (f *fakeNotify) snapshot() []NotifyEvent { f.mu.Lock() defer f.mu.Unlock() return append([]NotifyEvent(nil), f.events...) } func TestChatMentionsFireNotify(t *testing.T) { fc := &fakeChat{} fn := &fakeNotify{} r := ops.NewRegistry() repos := testRepos(wikiIndex(t)) mustRepo(t, repos, "test").Code = "c1" if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Chat: fc, Notify: fn}); err != nil { t.Fatal(err) } if _, err := r.Invoke(context.Background(), "chat.post", chatPostIn{Repo: "test", Text: "hey @dan and @eve, see @dan again"}); err != nil { t.Fatal(err) } // fireNotify is async; wait briefly for the goroutines. var evs []NotifyEvent for i := 0; i < 50; i++ { if evs = fn.snapshot(); len(evs) >= 2 { break } time.Sleep(20 * time.Millisecond) } got := map[string]bool{} for _, e := range evs { if e.Workflow != "quarry-mention" { t.Fatalf("unexpected workflow %q", e.Workflow) } got[e.To] = true } if !got["dan"] || !got["eve"] || len(got) != 2 { t.Fatalf("mention recipients = %v (want dan, eve, deduped)", got) } } // mentions() extracts distinct handles. func TestMentionsParse(t *testing.T) { got := mentions("ping @alice, @bob-1 and @alice again; email [email protected] is NOT a mention") want := map[string]bool{"alice": true, "bob-1": true} for _, g := range got { if !want[g] { t.Fatalf("unexpected mention %q in %v (email @ should not match)", g, got) } } if len(got) != 2 { t.Fatalf("mentions = %v (want alice, bob-1, deduped, no email)", got) } } // A fleet-wide search must never consult, count, or name a repository // the caller cannot read. Leaking through a search result is the same // leak as leaking through a listing, and the hit count moving with a // private repository's contents would leak it just as surely. func TestGlobalSearchExcludesUnreadableRepos(t *testing.T) { mk := func(t *testing.T, comment string) *index.Index { 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: strings.Repeat("5c", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T10:00:00", User: "dan", Comment: comment}}}); err != nil { t.Fatal(err) } return ix } open, secret := mk(t, "flamingo in the open"), mk(t, "flamingo in the vault") r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"open": {Index: open}, "secret": {Index: secret}}), CanRead: func(_ context.Context, repo string) bool { return repo != "secret" }, }); err != nil { t.Fatal(err) } out, err := r.Invoke(context.Background(), "search.all", searchAllIn{Q: "flamingo", Limit: 50}) if err != nil { t.Fatal(err) } got := out.(searchAllOut) if got.Searched != 1 { t.Errorf("searched %d repositories, want 1 — the private one was consulted", got.Searched) } for _, h := range got.Results { if h.Repo == "secret" { t.Fatalf("a private repository's content reached a fleet-wide result: %+v", h) } } for _, b := range got.ByRepo { if b.Repo == "secret" { t.Fatalf("a private repository was named in the facet: %+v", b) } } if len(got.Results) != 1 { t.Errorf("got %d results, want the one public match", len(got.Results)) } // Naming it directly must report NOT FOUND, never an empty result: // an empty answer says "exists but nothing matched". if _, err := r.Invoke(context.Background(), "search.all", searchAllIn{Q: "flamingo", Repo: "secret", Limit: 50}); !errors.Is(err, ops.ErrNotFound) { t.Errorf("naming a private repo gave %v, want ErrNotFound", err) } } // The facet must carry a true count, not the page size. A facet reading // the limit for every repository tells a reader nothing about where to // look, which is the only thing it exists to do. func TestGlobalSearchFacetCountsAreNotThePageSize(t *testing.T) { ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) var parsed []index.Parsed for i := range 7 { parsed = append(parsed, index.Parsed{ Hash: fmt.Sprintf("%02x%s", i, strings.Repeat("a1", 31)), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T10:00:00", User: "dan", Comment: fmt.Sprintf("pelican number %d", i)}}) } if err := ix.FoldNow(parsed); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"birds": {Index: ix}})}); err != nil { t.Fatal(err) } out, err := r.Invoke(context.Background(), "search.all", searchAllIn{Q: "pelican", Limit: 2}) if err != nil { t.Fatal(err) } got := out.(searchAllOut) if len(got.Results) != 2 { t.Fatalf("returned %d results, want the limit of 2", len(got.Results)) } if len(got.ByRepo) != 1 || got.ByRepo[0].Count != 7 { t.Errorf("facet = %+v, want birds counted 7 despite the limit of 2", got.ByRepo) } } // The people surface is built from names on artifacts a reader can // already see, so it exposes nothing new — but a repository they may // NOT read must contribute neither a name nor a count, or the listing // leaks that the repository exists and who works on it. func TestPeopleListExcludesUnreadableRepos(t *testing.T) { mk := func(user string) *index.Index { 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: strings.Repeat("7e", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-08-15T10:00:00", User: user, Comment: "work"}}}); err != nil { t.Fatal(err) } return ix } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{ "open": {Index: mk("visible")}, "secret": {Index: mk("hidden")}}), CanRead: func(_ context.Context, repo string) bool { return repo != "secret" }, }); err != nil { t.Fatal(err) } out, err := r.Invoke(context.Background(), "people.list", peopleListIn{Limit: 50}) if err != nil { t.Fatal(err) } got := out.(peopleListOut) for _, p := range got.People { if p.User == "hidden" { t.Fatalf("a private repository's contributor was listed: %+v", p) } if slices.Contains(p.Repos, "secret") { t.Fatalf("a private repository was named on a contributor: %+v", p) } } if got.Total != 1 { t.Errorf("total = %d, want 1 — the private repo's person was counted", got.Total) } } // One person working in several repositories is ONE person on the // fleet listing, with their events summed and their range spanning all // of them — not one row per repository. func TestPeopleListMergesAcrossRepos(t *testing.T) { mk := func(date string) *index.Index { 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: strings.Repeat("8f", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: date, User: "drh", Comment: "work"}}}); err != nil { t.Fatal(err) } return ix } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{ "early": {Index: mk("2001-01-01T00:00:00")}, "late": {Index: mk("2026-01-01T00:00:00")}, })}); err != nil { t.Fatal(err) } out, err := r.Invoke(context.Background(), "people.list", peopleListIn{Limit: 50}) if err != nil { t.Fatal(err) } got := out.(peopleListOut) if len(got.People) != 1 { t.Fatalf("got %d rows, want one merged person: %+v", len(got.People), got.People) } p := got.People[0] if p.Events != 2 { t.Errorf("events = %d, want 2 summed across repositories", p.Events) } if len(p.Repos) != 2 { t.Errorf("repos = %v, want both", p.Repos) } if p.First[:4] != "2001" || p.Last[:4] != "2026" { t.Errorf("range = %s..%s, want it to span both repositories", p.First, p.Last) } } // The fossil clone command is offered only where it would work. A // store holding a sub-manifest artifact is refused by peers below // Fossil 2.29, and zit documents that disabling emission "cannot // retire artifacts already written" — so the flag must default to // FALSE. Offering a command that fails is worse than withholding one // that would have worked. func TestFossilCloneDefaultsOff(t *testing.T) { ix, err := index.Open(filepath.Join(t.TempDir(), "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"unprobed": {Index: ix}})}); err != nil { t.Fatal(err) } out, err := r.Invoke(context.Background(), "repo.get", repoGetIn{Repo: "unprobed"}) if err != nil { t.Fatal(err) } if out.(repoInfo).FossilClone { t.Error("a repository nobody probed is advertised as fossil-cloneable") } } // .quarry/tickets.json is repo-authored content feeding sort order and // form suggestions. The parser's contract: unknown FIELDS are ignored // (a newer file against an older quarry must not break), unknown SHAPES // are an error (a typo must not silently become "defaults"), and // oversized input is refused rather than truncated (a silently // shortened list would reorder the tail of every sorted view). func TestParseTicketVocab(t *testing.T) { got, err := parseTicketVocab([]byte(`{ "priority": [" P0", "P1 ", ""], "team": ["red", "blue"], "severity": [] }`)) if err != nil { t.Fatal(err) } if !slices.Equal(got["priority"], []string{"P0", "P1"}) { t.Errorf("priority = %v, want trimmed [P0 P1]", got["priority"]) } if _, leaked := got["team"]; leaked { t.Error("an unknown field was not ignored") } if v, present := got["severity"]; !present || len(v) != 0 { t.Errorf("an explicit empty list must override (= no suggestions), got %v", v) } for name, bad := range map[string]string{ "not an object": `["P0"]`, "wrong values": `{"priority": "P0"}`, "word too long": `{"priority": ["` + strings.Repeat("x", 81) + `"]}`, "too many words": `{"priority": [` + strings.Repeat(`"w",`, 50) + `"w"]}`, } { if _, err := parseTicketVocab([]byte(bad)); err == nil { t.Errorf("%s: accepted", name) } } } // The detail surface is where a private ticket leaked whole: anyone // holding an id prefix got description, discussion and attachments. // ticket.get must answer NOT FOUND — indistinguishable from an id that // never existed, because a "forbidden" confirms there is something to // want. func TestTicketGetRefusesPrivateAsNotFound(t *testing.T) { ix := wikiIndex(t) priv := strings.Repeat("dd", 20) if err := ix.FoldNow([]index.Parsed{ {Hash: "pp1", Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: priv, Date: "2026-08-15T09:00:00", User: "dan", Fields: []artifact.Field{{Name: "title", Value: "secret"}, {Name: "status", Value: "Open"}, {Name: "is_private", Value: "1"}}}}, }); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil { t.Fatal(err) } _, err := r.Invoke(context.Background(), "ticket.get", ticketGetIn{Repo: "test", ID: priv[:8]}) if !errors.Is(err, ops.ErrNotFound) { t.Fatalf("ticket.get on a private ticket: %v, want ErrNotFound", err) } } // repo.reindex is the repair path that used to require stopping the // forge: the offline CLI and serve contend for the same files, so a // one-repo repair was priced at a whole-forge outage. In process it is // just an op — but one that wipes derived state, so it must be // admin-gated and must refuse cleanly where no pipeline is wired. func TestReindexOp(t *testing.T) { ix := wikiIndex(t) called := 0 stub := &Repo{Index: ix, Reindex: func() (int, error) { called++; return 42, nil }} repos := NewRepos(map[string]*Repo{"test": stub}) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: repos}); err != nil { t.Fatal(err) } d, ok := r.Lookup("repo.reindex") if !ok || d.Auth != ops.Admin { t.Fatalf("repo.reindex auth = %v, want Admin — this op wipes derived state", d) } got, err := r.Invoke(context.Background(), "repo.reindex", reindexIn{Repo: "test"}) if err != nil { t.Fatal(err) } if out := got.(reindexOut); out.Artifacts != 42 || called != 1 { t.Fatalf("out=%+v called=%d", out, called) } stub.Reindex = nil if _, err := r.Invoke(context.Background(), "repo.reindex", reindexIn{Repo: "test"}); !errors.Is(err, ops.ErrInvalid) { t.Fatalf("nil pipeline: %v, want ErrInvalid", err) } } // parseRepoConfig guards the same boundary parseTicketVocab does: a // repo-authored file that feeds links and labels. The homepage must be // http(s) — javascript: in an <a href> on every landing view is the // attack this check exists for — and a tab without a label names // itself from its filename stem. func TestParseRepoConfig(t *testing.T) { home, topics, docs, err := parseRepoConfig([]byte(`{ "homepage": "https://example.org", "topics": [" ci ", "forge"], "docs": [{"path": "docs/INSTALL.md"}, {"path": "README.md", "label": "Read Me"}] }`)) if err != nil { t.Fatal(err) } if home != "https://example.org" || topics[0] != "ci" { t.Fatalf("home=%q topics=%v", home, topics) } if docs[0].Label != "INSTALL" || docs[1].Label != "Read Me" { t.Fatalf("labels = %+v", docs) } for name, bad := range map[string]string{ "javascript homepage": `{"homepage": "javascript:alert(1)"}`, "not an object": `[1]`, "pathless tab": `{"docs": [{"label": "x"}]}`, } { if _, _, _, err := parseRepoConfig([]byte(bad)); err == nil { t.Errorf("%s: accepted", name) } } } // The convention discovers marker documents at the ROOT only, dedupes // LICENSE/COPYING into one tab, and README always leads: it is the // default tab, and a panel that opens on SECURITY.md would be a strange // front door. func TestMarkerDocs(t *testing.T) { docs := markerDocs([]zitcli.LsEntry{ {Name: "SECURITY.md"}, {Name: "docs/README.md"}, {Name: "readme.txt"}, {Name: "COPYING"}, {Name: "LICENSE.txt"}, {Name: "main.go"}, }) var got []string for _, d := range docs { got = append(got, d.Label+":"+d.Path) } want := []string{"README:readme.txt", "License:LICENSE.txt", "Security:SECURITY.md"} if !slices.Equal(got, want) { t.Fatalf("docs = %v, want %v", got, want) } } // stubAccounts is the minimum AccountStore a dashboard test needs. type stubAccounts struct{ grants map[string]map[string]string } func (s stubAccounts) Accounts() ([]Account, error) { return nil, nil } func (s stubAccounts) AddAccount(string, string, bool) error { return nil } func (s stubAccounts) SetPassword(string, string) error { return nil } func (s stubAccounts) DeleteAccount(string) error { return nil } func (s stubAccounts) Grant(string, string, string) error { return nil } func (s stubAccounts) Revoke(string, string) error { return nil } func (s stubAccounts) GrantsFor(a string) (map[string]string, error) { return s.grants[a], nil } // me.feed is the signed-in dashboard: grant-scoped, so a reader sees // THEIR repos and their merged activity — and only theirs. The caller // comes from the request context, never from an input field, so nobody // asks for someone else's grants by name. func TestMeFeed(t *testing.T) { mk := func(t *testing.T, seedUser, date, comment string) *index.Index { ix := wikiIndex(t) if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat(comment[:1], 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: date, User: seedUser, Comment: comment}}}); err != nil { t.Fatal(err) } return ix } repos := NewRepos(map[string]*Repo{ "alpha": {Index: mk(t, "dan", "2026-08-19T10:00:00", "a newest")}, "beta": {Index: mk(t, "eve", "2026-08-18T10:00:00", "b older")}, "gamma": {Index: mk(t, "dan", "2026-08-20T10:00:00", "c ungrannted")}, }) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Accounts: stubAccounts{grants: map[string]map[string]string{ "dan": {"alpha": "write", "beta": "read"}, }}}); err != nil { t.Fatal(err) } got, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "me.feed", meFeedIn{}) if err != nil { t.Fatal(err) } out := got.(meFeedOut) if len(out.Repos) != 2 || out.Repos[0].Name != "alpha" { t.Fatalf("repos = %+v, want [alpha beta] by activity; gamma is not granted", out.Repos) } if len(out.Feed) < 2 || out.Feed[0].Comment != "a newest" || out.Feed[1].Comment != "b older" { t.Fatalf("feed not merged newest-first: %+v", out.Feed) } for _, e := range out.Feed { if e.Repo == "gamma" { t.Fatalf("an ungranted repo's event leaked into the feed: %+v", e) } } // Mine: string-equality on the author name; dan's newest leads and // nothing of eve's appears. if len(out.Mine) == 0 || out.Mine[0].Comment != "a newest" { t.Fatalf("mine = %+v, want dan's check-in first", out.Mine) } for _, e := range out.Mine { if e.User != "dan" { t.Fatalf("someone else's work in mine: %+v", e) } } // No identity, no feed. if _, err := r.Invoke(context.Background(), "me.feed", meFeedIn{}); !errors.Is(err, ops.ErrNotFound) { t.Fatalf("anonymous me.feed: %v, want ErrNotFound", err) } } // A repository's own documentation lives in its TREE — fossil keeps // every page of www/ there, not as wiki artifacts — so which extensions // are documents is a contract, not a detail. file.get answers the same // question from the same table, or the source view offers a rendered // form that does not exist. func TestDocMimetypeMatchesFossilsOwnDispatch(t *testing.T) { for _, tc := range []struct { path string want string ok bool }{ {"www/index.wiki", "", true}, // absent mimetype IS fossil wiki {"README.md", "text/x-markdown", true}, {"doc/guide.markdown", "text/x-markdown", true}, {"www/permutedindex.html", "text/html", true}, {"www/page.HTM", "text/html", true}, // extensions are case-folded {"notes.txt", "text/plain", true}, {"diagram.pikchr", "text/x-pikchr", true}, {"src/main.c", "", false}, // source stays source {"Makefile", "", false}, // no extension at all {"logo.png", "", false}, } { got, ok := docMimetype(tc.path) if ok != tc.ok || got != tc.want { t.Errorf("docMimetype(%q) = %q,%v; want %q,%v", tc.path, got, ok, tc.want, tc.ok) } } } // Repository-authored HTML renders, but through the sanitizer every // other rendered path uses. Fossil serves it closer to as-is and warns // that a script inside it trips the browser's CSP; quarry serves other // people's repositories, so the gate stays shut. func TestEmbeddedHTMLIsRenderedButSanitized(t *testing.T) { body := []byte(`<h2>Title</h2><p>ok</p><script>alert(1)</script>` + `<a href="javascript:alert(2)">bad</a>`) got, err := renderBody(nil, "repo", "text/html", body) if err != nil { t.Fatal(err) } if !strings.Contains(got, "<h2>Title</h2>") || !strings.Contains(got, "<p>ok</p>") { t.Errorf("the document did not render:\n%s", got) } if strings.Contains(got, "<script") || strings.Contains(got, "alert(1)") { t.Errorf("a script survived the sanitizer:\n%s", got) } if strings.Contains(got, "javascript:") { t.Errorf("a javascript: href survived the sanitizer:\n%s", got) } } // ticket.across is the tracker page asked of every repository at once. // Its whole reason to exist is that a person's work does not live in one // repository, so the two things it must get right are the merge (rows // carry their tracker, counts and facets sum across them) and the scope // (a tracker you were not granted contributes nothing, and naming it // explicitly does not change that). func TestTicketAcross(t *testing.T) { mk := func(t *testing.T, sub, author, date, title string) *index.Index { ix := wikiIndex(t) id := strings.Repeat(title[:1], 40) if err := ix.FoldNow([]index.Parsed{{Hash: title[:4], Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: id, Date: date, User: author, Fields: []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: title}, {Name: "subsystem", Value: sub}}}}}); err != nil { t.Fatal(err) } return ix } repos := NewRepos(map[string]*Repo{ "alpha": {Index: mk(t, "web", "dan", "2026-08-19T10:00:00", "alpha bug")}, "beta": {Index: mk(t, "web", "eve", "2026-08-20T10:00:00", "beta bug")}, "gamma": {Index: mk(t, "api", "dan", "2026-08-21T10:00:00", "gamma bug")}, }) r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Accounts: stubAccounts{grants: map[string]map[string]string{ "dan": {"alpha": "write", "beta": "read"}, }}}); err != nil { t.Fatal(err) } ctx := ops.WithUser(context.Background(), "dan") got, err := r.Invoke(ctx, "ticket.across", ticketAcrossIn{}) if err != nil { t.Fatal(err) } out := got.(ticketAcrossOut) if out.Count != 2 { t.Fatalf("count = %d, want 2: gamma is not granted", out.Count) } // Newest-updated first, and every row knows its tracker — without // that the listing cannot be clicked through to anything. if out.Tickets[0].Repo != "beta" || out.Tickets[1].Repo != "alpha" { t.Fatalf("order/repos = %+v, want beta then alpha", out.Tickets) } if out.Open != 2 { t.Fatalf("open = %d, want the two granted trackers summed", out.Open) } // Facets sum across trackers: one "web" with the work of both. if len(out.Subsystems) != 1 || out.Subsystems[0].Value != "web" || out.Subsystems[0].Open != 2 { t.Fatalf("subsystems = %+v, want web=2 merged", out.Subsystems) } if len(out.Authors) != 2 { t.Fatalf("authors = %+v, want dan and eve", out.Authors) } // Repository is the axis that only exists here. if len(out.Repos) != 2 { t.Fatalf("repo facet = %+v, want the two granted trackers", out.Repos) } // Repo is a facet, not addressing: it narrows within what you may // already see. got, err = r.Invoke(ctx, "ticket.across", ticketAcrossIn{Repo: "alpha"}) if err != nil { t.Fatal(err) } if out := got.(ticketAcrossOut); out.Count != 1 || out.Tickets[0].Repo != "alpha" { t.Fatalf("repo facet = %+v, want alpha alone", out.Tickets) } // Naming a tracker you were not granted returns the empty listing — // never someone else's tickets, and never an error that confirms the // repository exists. got, err = r.Invoke(ctx, "ticket.across", ticketAcrossIn{Repo: "gamma"}) if err != nil { t.Fatal(err) } if out := got.(ticketAcrossOut); out.Count != 0 { t.Fatalf("ungranted tracker leaked %+v", out.Tickets) } // The other report axes still work, one repository deep or three. got, err = r.Invoke(ctx, "ticket.across", ticketAcrossIn{Author: "eve"}) if err != nil { t.Fatal(err) } if out := got.(ticketAcrossOut); out.Count != 1 || out.Tickets[0].Repo != "beta" { t.Fatalf("author axis = %+v", out.Tickets) } // No identity, no listing. if _, err := r.Invoke(context.Background(), "ticket.across", ticketAcrossIn{}); !errors.Is(err, ops.ErrNotFound) { t.Fatalf("anonymous ticket.across: %v, want ErrNotFound", err) } } // Awaiting is the landing page's question: of the tickets I opened, // which has somebody else answered and left with me? A ticket nobody // has touched since I filed it is not owed a reply, and neither is one // whose last word was my own — those two exclusions are the whole rule, // and getting either wrong turns the band into a second inbox of noise. func TestTicketAcrossAwaiting(t *testing.T) { ix := wikiIndex(t) fold := func(id, author, lastUser, date, title string) { batch := []index.Parsed{{Hash: id[:4], Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: id, Date: date, User: author, Fields: []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: title}}}}} if lastUser != "" { batch = append(batch, index.Parsed{Hash: id[:4] + "z", Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: id, Date: date[:11] + "23:00:00", User: lastUser, Fields: []artifact.Field{{Name: "status", Value: "Open"}}}}) } if err := ix.FoldNow(batch); err != nil { t.Fatal(err) } } fold(strings.Repeat("a", 40), "dan", "eve", "2026-08-19T09:00:00", "eve answered me") fold(strings.Repeat("b", 40), "dan", "", "2026-08-19T09:00:00", "nobody has answered") fold(strings.Repeat("c", 40), "dan", "dan", "2026-08-19T09:00:00", "I spoke last") fold(strings.Repeat("d", 40), "eve", "dan", "2026-08-19T09:00:00", "not my ticket") r := ops.NewRegistry() if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix), Accounts: stubAccounts{grants: map[string]map[string]string{ "dan": {"test": "write"}, }}}); err != nil { t.Fatal(err) } got, err := r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.across", ticketAcrossIn{Awaiting: true}) if err != nil { t.Fatal(err) } out := got.(ticketAcrossOut) if out.Count != 1 || out.Tickets[0].Title != "eve answered me" { var titles []string for _, t := range out.Tickets { titles = append(titles, t.Title) } t.Fatalf("awaiting = %v, want only the one somebody else answered", titles) } // The header must describe exactly the rows beneath it: a count the // rule cannot produce is a count that lies. if out.Open != 1 || out.Closed != 0 { t.Fatalf("counts = %d/%d, want the listing's own size", out.Open, out.Closed) } // Awaiting owns author and state; it cannot be bent into someone // else's inbox. got, err = r.Invoke(ops.WithUser(context.Background(), "dan"), "ticket.across", ticketAcrossIn{Awaiting: true, Author: "eve", State: "all"}) if err != nil { t.Fatal(err) } if out := got.(ticketAcrossOut); out.Count != 1 || out.Tickets[0].Author != "dan" { t.Fatalf("awaiting answered for someone else: %+v", out.Tickets) } } // Fossil's own documentation links directories with a trailing slash — // www/index.wiki carries [./server/|a variety of techniques] — so a // directory has to answer with its index the way every web server has. // Sending those to the file view produced a 404 with a real page // sitting behind it. func TestDocIndexNamesPreferRenderedFormats(t *testing.T) { // Preference order matters: a directory holding both index.md and // index.html should serve the one that renders into the page. md, html := -1, -1 for i, n := range docIndexNames { switch n { case "index.md": md = i case "index.html": html = i } } if md < 0 || html < 0 { t.Fatalf("docIndexNames lost a candidate: %v", docIndexNames) } if md > html { t.Errorf("index.html is preferred over index.md: %v", docIndexNames) } // Every candidate must actually be renderable, or a directory would // resolve to a document doc.get then refuses. for _, n := range docIndexNames { if _, ok := docMimetype(n); !ok { t.Errorf("index candidate %q is not a renderable document", n) } } } // Fossil content links its own documentation absolutely — // [/doc/trunk/www/inout.wiki|importing from Git] appears in fossil's // Cookbook — and those must land on the RENDERED document. The mapping // pointed at the file route while that was the only route there was; // once doc.get existed, it was quietly sending readers to source // listings of pages quarry can render. func TestFossilDocLinksResolveToTheRenderedDocument(t *testing.T) { res := resolver{repo: "fossil"} for _, tc := range []struct{ in, want string }{ {"/doc/trunk/www/inout.wiki", "/r/fossil/doc/trunk/www/inout.wiki"}, {"/doc/trunk/www/server/any/cgi.md", "/r/fossil/doc/trunk/www/server/any/cgi.md"}, {"/doc/abc123/README.md", "/r/fossil/doc/abc123/README.md"}, } { if got := res.ResolvePath(tc.in); got != tc.want { t.Errorf("ResolvePath(%q) = %q, want %q", tc.in, got, tc.want) } } // Unversioned content is a fossil feature quarry does not have, and // a dropped link beats one that 404s — this is why "Download" in // fossil's own index renders as plain text. if got := res.ResolvePath("/uv/download.html"); got != "" { t.Errorf("ResolvePath(/uv/...) = %q, want a dropped link", got) } }