Artifact 037eda6220d48c14b84d92c7bfdcec79c1a246d614f78eb54d87b4474d392d5f:
- File
internal/api/register_test.go
— part of check-in
[420206e59f]
at
2026-08-21 20:40:05
on branch trunk
— markdown composer: Write/Preview tabs, slash commands, described check-in links; forum hashes resolve
Preview runs the production render pipeline (markup.preview op). Check-in refs carry their comment as hover title + visible excerpt. composer.js loads only on composer pages — the base stays one-script. (user: dmestas, size: 44629) [annotate] [blame] [check-ins using]
package api import ( "context" "craftdesign.group/quarry/internal/zitcli" "errors" "fmt" "path/filepath" "slices" "strings" "sync" "testing" "time" "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", Closed: true}) 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(10, "", "", "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) } }