Artifact a2d80ec343ec49a9d5cf382fc71c3d0bbd27fecc20e3a3525f9aa0ed014d2afd:
- File
internal/api/register_test.go
— part of check-in
[6da2e08171]
at
2026-08-15 19:01:56
on branch trunk
— index: WAL + one branch recompute per fold round
Per-batch recursive-CTE recompute was quadratic over bulk ingest; rollback-journal mode locked readers out during folds. Adaptive fold MaxWait so backlog delivery lag isn't mistaken for drained. (user: dmestas, size: 17177) [annotate] [blame] [check-ins using]
package api import ( "context" "errors" "fmt" "path/filepath" "strings" "testing" "craftdesign.group/quarry/internal/artifact" "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 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 if !strings.Contains(html, `<a href="/r/test/checkin/`+h2+`">`+h2[:12]+`</a>`) { t.Fatalf("hash ref not resolved:\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) } got, err := r.Invoke(context.Background(), "ticket.list", ticketListIn{Repo: "test"}) 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) } } func testRepos(ix *index.Index) map[string]*Repo { return 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: 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) } } }