Artifact 5f34bcd776d945ecae4161a29685f891d5eee1ffb514ebd46f78d2c6c4dad857:
- File
internal/index/index_test.go
— part of check-in
[7cfce7761b]
at
2026-08-22 16:46:10
on branch ticket-list
— tickets: report filters, and chips that mute what every row shares
Six report axes as native selects in one GET form; the URL is the report, so a listing is shareable and scriptable with no query language. Counts run through the same filter and every header link preserves it.
Chips mute a value nearly every listed row shares — "Fixed" on 34 of 34 closed rows distinguished nothing. (user: dmestas, size: 13151) [annotate] [blame] [check-ins using]
package index import ( "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. subs, authors, err := ix.TicketFacets() if err != nil { t.Fatal(err) } if strings.Join(subs, ",") != "api,web" { t.Errorf("subsystem facets = %v, want api,web sorted", subs) } if strings.Join(authors, ",") != "ana,dan" { t.Errorf("author facets = %v, want ana,dan sorted", authors) } }