Artifact 0f60d27408e605ffa3456f0598ee2b53c38b66c75950719961a46d50e5aa5c30:
- File
internal/index/index_test.go
— part of check-in
[15c58d6fe0]
at
2026-08-22 20:46:56
on branch ticket-list
— tickets: left rail — views, subsystems and people with open counts
Every entry is an ordinary report URL, so the rail stores nothing and promises nothing the address bar cannot say. Browse groups count OPEN work and sort by it; clicking the entry you are on removes that axis. Hidden under 64rem, where the dropdowns already reach every value. (user: dmestas, size: 15361) [annotate] [blame] [check-ins using] [more...]
package index import ( "fmt" "strings" "testing" "craftdesign.group/quarry/internal/artifact" ) // The contributor table is folded at write time, so it must agree with // the aggregate it replaces — for every event kind, not just check-ins. // A person who only files tickets is a contributor. func TestContributorsMatchTheAggregate(t *testing.T) { ix := openIndex(t) parsed := []Parsed{ {Hash: strings.Repeat("a1", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2020-01-01T00:00:00", User: "drh", Comment: "first"}}, {Hash: strings.Repeat("a2", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-01-01T00:00:00", User: "drh", Comment: "later"}}, {Hash: strings.Repeat("a3", 32), Art: &artifact.Artifact{Type: artifact.Wiki, WikiTitle: "Guide", Date: "2023-01-01T00:00:00", User: "dan", Body: []byte("x")}}, } if err := ix.FoldNow(parsed); err != nil { t.Fatal(err) } got, err := ix.Contributors(50) if err != nil { t.Fatal(err) } if len(got) != 2 { t.Fatalf("got %d contributors, want 2: %+v", len(got), got) } // Busiest first. if got[0].User != "drh" || got[0].Events != 2 { t.Errorf("first = %+v, want drh with 2 events", got[0]) } // A wiki-only author still counts. if got[1].User != "dan" || got[1].Events != 1 { t.Errorf("second = %+v, want dan with 1 event", got[1]) } if got[0].First[:4] != "2020" || got[0].Last[:4] != "2026" { t.Errorf("drh range = %s..%s, want 2020..2026", got[0].First, got[0].Last) } // And it must equal the aggregate it stands in for. rows, err := ix.db.Query(`SELECT user, count(*) FROM event GROUP BY user`) if err != nil { t.Fatal(err) } defer rows.Close() want := map[string]int{} for rows.Next() { var u string var n int if err := rows.Scan(&u, &n); err != nil { t.Fatal(err) } want[u] = n } for _, c := range got { if want[c.User] != c.Events { t.Errorf("%s: folded %d events, aggregate says %d", c.User, c.Events, want[c.User]) } } } // Reset must leave the folded tables exact, not merely consistent: the // delete trigger deliberately does not narrow first/last, so a rebuild // is the only thing that restores them precisely. func TestResetRebuildsContributorsExactly(t *testing.T) { ix := openIndex(t) if err := ix.FoldNow([]Parsed{ {Hash: strings.Repeat("b1", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2001-01-01T00:00:00", User: "old", Comment: "ancient"}}, }); err != nil { t.Fatal(err) } if err := ix.Reset(); err != nil { t.Fatal(err) } got, err := ix.Contributors(50) if err != nil { t.Fatal(err) } if len(got) != 0 { t.Errorf("after Reset: %+v, want no contributors", got) } if err := ix.FoldNow([]Parsed{ {Hash: strings.Repeat("b2", 32), Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-06-06T00:00:00", User: "new", Comment: "fresh"}}, }); err != nil { t.Fatal(err) } got, _ = ix.Contributors(50) if len(got) != 1 || got[0].User != "new" || got[0].First[:4] != "2026" { t.Errorf("after rebuild: %+v, want only new starting 2026", got) } } // A tracker's list is a working surface. Across this corpus most of // every tracker is finished work — sqlite currently has 2 open tickets // against 792 closed — so closed ones are opt-in. The vocabulary is // per-repository, so an unrecognized status must count as OPEN: a // wrongly-shown ticket is noise, a wrongly-hidden one is invisible. func TestClosedTicketsAreOptIn(t *testing.T) { ix := openIndex(t) mk := func(hash, status string) Parsed { return Parsed{Hash: hash, Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: hash, Date: "2026-08-15T10:00:00", User: "dan", Fields: []artifact.Field{ {Name: "title", Value: "t-" + status}, {Name: "status", Value: status}}, }} } if err := ix.FoldNow([]Parsed{ mk(strings.Repeat("a1", 20), "Open"), mk(strings.Repeat("a2", 20), "Closed"), mk(strings.Repeat("a3", 20), "closed"), // casing varies in real data mk(strings.Repeat("a4", 20), "Fixed"), mk(strings.Repeat("a5", 20), "Needs Triage"), // unknown word }); err != nil { t.Fatal(err) } open, _, err := ix.TicketSummaries(50, "", TicketFilter{State: "open"}) if err != nil { t.Fatal(err) } got := map[string]bool{} for _, s := range open { got[s.Status] = true } for _, want := range []string{"Open", "Needs Triage"} { if !got[want] { t.Errorf("%q is missing from the open listing; an unrecognized status must not hide a ticket", want) } } for _, hidden := range []string{"Closed", "closed", "Fixed"} { if got[hidden] { t.Errorf("%q appeared in the open listing", hidden) } } all, _, err := ix.TicketSummaries(50, "", TicketFilter{State: "all"}) if err != nil { t.Fatal(err) } if len(all) != 5 { t.Errorf("opting in returned %d tickets, want all 5", len(all)) } // And the finished work browsable on its own, which is the third // tab every forge offers. done, _, err := ix.TicketSummaries(50, "", TicketFilter{State: "closed"}) if err != nil { t.Fatal(err) } if len(done) != 3 { t.Errorf("closed-only returned %d tickets, want the 3 finished ones", len(done)) } } // Fossil can mark a whole ticket private. quarry indexed the field and // gave it no meaning, so 67 tickets across the served corpus were // listed, searchable and readable by anonymous callers. A private // ticket must be absent from all three, and absent means NOT FOUND — // a refusal would confirm it exists. func TestPrivateTicketsAreNotServed(t *testing.T) { ix := openIndex(t) mk := func(hash, title, private string) Parsed { f := []artifact.Field{{Name: "title", Value: title}, {Name: "status", Value: "Open"}} if private != "" { f = append(f, artifact.Field{Name: "is_private", Value: private}) } return Parsed{Hash: hash, Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: hash, Date: "2026-08-15T10:00:00", User: "dan", Fields: f}} } if err := ix.FoldNow([]Parsed{ mk(strings.Repeat("c1", 20), "public pelican", ""), mk(strings.Repeat("c2", 20), "public flamingo", "0"), mk(strings.Repeat("c3", 20), "secret heron", "1"), mk(strings.Repeat("c4", 20), "secret egret", "true"), }); err != nil { t.Fatal(err) } list, _, err := ix.TicketSummaries(50, "", TicketFilter{State: "all"}) if err != nil { t.Fatal(err) } if len(list) != 2 { t.Errorf("listing returned %d tickets, want the 2 public ones: %+v", len(list), list) } for _, s := range list { if strings.HasPrefix(s.Title, "secret") { t.Errorf("a private ticket was listed: %+v", s) } } // Reachable by id? It must report not-found, not forbidden. if _, ok, err := ix.TicketByPrefix(strings.Repeat("c3", 20)); err != nil || ok { t.Errorf("a private ticket resolved by id (ok=%v)", ok) } if _, ok, err := ix.TicketByPrefix(strings.Repeat("c1", 20)); err != nil || !ok { t.Errorf("a public ticket stopped resolving (ok=%v)", ok) } // Searchable? A snippet leaks the content whether or not the page is. hits, err := ix.Search("secret", "ticket", 50) if err != nil { t.Fatal(err) } if len(hits) != 0 { t.Errorf("a private ticket is in the search corpus: %+v", hits) } if hits, _ := ix.Search("pelican", "ticket", 50); len(hits) != 1 { t.Errorf("public tickets stopped being searchable (%d hits)", len(hits)) } } // SQLite discards a conflict clause written inside a trigger body when // the firing statement carries its own — and check-ins arrive through // an upsert. With "INSERT OR REPLACE INTO event" in the trigger, // re-folding an artifact already known aborted the entire batch with a // UNIQUE failure on event.hash, silently stopping ingest on every // restart whose frame cursor overlapped the index. func TestRefoldingAKnownArtifactSucceeds(t *testing.T) { ix := openIndex(t) batch := []Parsed{{Hash: strings.Repeat("d1", 32), Art: &artifact.Artifact{ Type: artifact.Manifest, Date: "2026-08-15T10:00:00", User: "dan", Comment: "once"}}} if err := ix.FoldNow(batch); err != nil { t.Fatal(err) } // The same artifact again, as a replayed frame cursor delivers it. if err := ix.FoldNow(batch); err != nil { t.Fatalf("re-folding a known artifact failed: %v", err) } var n int if err := ix.db.QueryRow(`SELECT count(*) FROM event WHERE hash = ?`, strings.Repeat("d1", 32)).Scan(&n); err != nil { t.Fatal(err) } if n != 1 { t.Errorf("event rows for one artifact = %d, want 1", n) } } func TestDefaultBranchPrefersNewestAlias(t *testing.T) { ix := openIndex(t) fold := func(hash, branch, date string) { if _, err := ix.db.Exec(`INSERT INTO checkin (hash, date, user, comment, branch) VALUES (?, ?, 'u', 'c', ?)`, hash, date, branch); err != nil { t.Fatal(err) } } if got := ix.DefaultBranch(); got != "trunk" { t.Fatalf("empty repo default = %q, want trunk", got) } fold("a1", "trunk", "2011-03-05T00:00:00") if got := ix.DefaultBranch(); got != "trunk" { t.Fatalf("trunk-only = %q", got) } // The rename: work continues on main; the literal trunk stays. fold("b2", "main", "2026-08-01T00:00:00") if got := ix.DefaultBranch(); got != "main" { t.Fatalf("renamed repo = %q, want main", got) } // A feature branch newer than both changes nothing. fold("c3", "wild-idea", "2026-08-15T00:00:00") if got := ix.DefaultBranch(); got != "main" { t.Fatalf("feature branch stole the default: %q", got) } } // The list row names who opened a ticket, who touched it last, and how // many remarks it carries — folded with the summary, so the list stays // an index walk. func TestTicketSummaryCarriesPeopleAndRemarks(t *testing.T) { ix := openIndex(t) id := strings.Repeat("b1", 20) change := func(hash, date, user string, fields ...artifact.Field) Parsed { return Parsed{Hash: hash, Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: id, Date: date, User: user, Fields: fields}} } if err := ix.FoldNow([]Parsed{ change(strings.Repeat("c1", 20), "2026-08-15T10:00:00", "ann", artifact.Field{Name: "title", Value: "t"}, artifact.Field{Name: "status", Value: "Open"}, artifact.Field{Name: "comment", Value: "the description"}), change(strings.Repeat("c2", 20), "2026-08-16T10:00:00", "bob", artifact.Field{Name: "icomment", Value: "a remark"}), change(strings.Repeat("c3", 20), "2026-08-17T10:00:00", "cid", artifact.Field{Name: "priority", Value: "High"}), change(strings.Repeat("c4", 20), "2026-08-18T10:00:00", "dee", artifact.Field{Name: "icomment", Value: "another"}), }); err != nil { t.Fatal(err) } got, _, err := ix.TicketSummaries(10, "", TicketFilter{State: "open"}) if err != nil || len(got) != 1 { t.Fatalf("got %d rows, err %v", len(got), err) } if got[0].Author != "ann" || got[0].LastUser != "dee" || got[0].Comments != 2 { t.Errorf("author %q last %q comments %d; want ann dee 2", got[0].Author, got[0].LastUser, got[0].Comments) } } // A report is the URL: each axis narrows the listing, and the header's // counts must be counted through the SAME filter. A header reading "37 // open" above four rows is two numbers a reader cannot reconcile, and // it was the first thing wrong with the old page. func TestReportAxesNarrowListingAndCountsTogether(t *testing.T) { ix := openIndex(t) mk := func(hash, status, typ, sub, who string) Parsed { return Parsed{Hash: hash, Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: hash, Date: "2026-08-15T10:00:00", User: who, Fields: []artifact.Field{ {Name: "title", Value: "t-" + hash[:2]}, {Name: "status", Value: status}, {Name: "type", Value: typ}, {Name: "subsystem", Value: sub}}, }} } if err := ix.FoldNow([]Parsed{ mk(strings.Repeat("b1", 20), "Open", "Code_Defect", "web", "dan"), mk(strings.Repeat("b2", 20), "Open", "Code_Defect", "api", "dan"), mk(strings.Repeat("b3", 20), "Fixed", "Code_Defect", "web", "ana"), mk(strings.Repeat("b4", 20), "Open", "Feature_Request", "web", "ana"), }); err != nil { t.Fatal(err) } web := TicketFilter{Subsystem: "web"} got, _, err := ix.TicketSummaries(50, "", web) if err != nil { t.Fatal(err) } if len(got) != 2 { // b1 and b4; b3 is closed, b2 is api t.Fatalf("subsystem=web open listing has %d rows, want 2", len(got)) } open, closed, err := ix.TicketCounts(web) if err != nil { t.Fatal(err) } if open != 2 || closed != 1 { t.Errorf("counts through subsystem=web = %d/%d, want 2/1 — the header must describe the listing", open, closed) } // Axes compose, and match case-insensitively like every other // status comparison in this file. both, _, err := ix.TicketSummaries(50, "", TicketFilter{Subsystem: "WEB", Type: "feature_request"}) if err != nil { t.Fatal(err) } if len(both) != 1 || both[0].Type != "Feature_Request" { t.Errorf("two composed axes returned %+v, want only the web Feature_Request", both) } // The facets offer what the tracker HOLDS, not what a config // guessed, counted by OPEN work and ordered by it: a browse list // answers "where is the work" before "what exists". web has two // open (b1, b4) against api's one, so web leads despite the alphabet. subs, authors, err := ix.TicketFacets() if err != nil { t.Fatal(err) } var facets []string for _, f := range subs { facets = append(facets, fmt.Sprintf("%s=%d", f.Value, f.Open)) } if strings.Join(facets, ",") != "web=2,api=1" { t.Errorf("subsystem facets = %v, want web=2,api=1", facets) } // b3 is ana's and closed, so ana counts one open (b4) and dan one // (b1 or b2 — both open); the tie falls back to the alphabet. facets = nil for _, f := range authors { facets = append(facets, fmt.Sprintf("%s=%d", f.Value, f.Open)) } if strings.Join(facets, ",") != "dan=2,ana=1" { t.Errorf("author facets = %v, want dan=2,ana=1", facets) } } // A column header names a KEY, and the SQL it selects lives in this // package. A key that is not in the whitelist sorts by the default: // a listing reached from a stale link should degrade, not 500. func TestTicketOrderIsAWhitelist(t *testing.T) { ix := openIndex(t) mk := func(hash, title, status string) Parsed { return Parsed{Hash: hash, Art: &artifact.Artifact{ Type: artifact.Ticket, TicketID: hash, Date: "2026-08-15T10:00:00", User: "dan", Fields: []artifact.Field{ {Name: "title", Value: title}, {Name: "status", Value: status}}, }} } if err := ix.FoldNow([]Parsed{ mk(strings.Repeat("c1", 20), "charlie", "Open"), mk(strings.Repeat("c2", 20), "alpha", "Open"), mk(strings.Repeat("c3", 20), "bravo", "Open"), }); err != nil { t.Fatal(err) } titles := func(f TicketFilter) []string { got, _, err := ix.TicketSummaries(50, "", f) if err != nil { t.Fatal(err) } var out []string for _, s := range got { out = append(out, s.Title) } return out } if got := strings.Join(titles(TicketFilter{Order: "title"}), ","); got != "alpha,bravo,charlie" { t.Errorf("order=title gave %q", got) } // Not in the map, and shaped like an injection attempt: both must // land on the default order rather than reaching the query. for _, bogus := range []string{"nonsense", "title; DROP TABLE ticket", "updated DESC--"} { if got := titles(TicketFilter{Order: bogus}); len(got) != 3 { t.Errorf("order=%q returned %d rows, want the default listing's 3", bogus, len(got)) } } if _, ok := TicketOrders["title"]; !ok { t.Error("the table's columns must each name a key of TicketOrders") } }