package index
import (
"testing"
"craftdesign.group/quarry/internal/artifact"
)
func checkinBy(hash, user, date string) Parsed {
return Parsed{Hash: hash, Art: &artifact.Artifact{
Type: artifact.Manifest, Date: date, User: user, Comment: "c " + hash,
}}
}
// Summary is a narrower answer to a question Counts also answers, so
// the two must never disagree — a fleet row and the repository page it
// links to show the same numbers side by side.
func TestSummaryAgreesWithTheCensus(t *testing.T) {
ix := openIndex(t)
if err := ix.FoldNow([]Parsed{
checkinBy("c1", "dan", "2026-01-01T00:00:00"),
checkinBy("c2", "eve", "2026-01-02T00:00:00"),
checkinBy("c3", "dan", "2026-01-03T00:00:00"),
wikiEdit("w1", "Home", "2026-01-04T00:00:00"),
}); err != nil {
t.Fatal(err)
}
sm, err := ix.Summary()
if err != nil {
t.Fatal(err)
}
c, err := ix.Counts(false)
if err != nil {
t.Fatal(err)
}
if sm.Checkins != c.Checkins || sm.Contributors != c.Contributors {
t.Fatalf("summary %+v disagrees with census checkins=%d contributors=%d",
sm, c.Checkins, c.Contributors)
}
if sm.Checkins != 3 {
t.Errorf("checkins=%d, want 3", sm.Checkins)
}
// dan, eve, and the wiki edit's author — distinct authors of
// anything, which is what the fleet row means by "contributors".
if sm.Contributors != 2 {
t.Errorf("contributors=%d, want 2 (dan, eve)", sm.Contributors)
}
}
// Contributors now reads the maintained contributor table instead of a
// DISTINCT scan over every event. Same answer or the change is a bug,
// and the fold triggers are what has to keep it true.
func TestContributorTableMatchesDistinctAuthors(t *testing.T) {
ix := openIndex(t)
if err := ix.FoldNow([]Parsed{
checkinBy("c1", "dan", "2026-01-01T00:00:00"),
checkinBy("c2", "eve", "2026-01-02T00:00:00"),
checkinBy("c3", "dan", "2026-01-03T00:00:00"),
wikiEdit("w1", "Home", "2026-01-04T00:00:00"),
}); err != nil {
t.Fatal(err)
}
var distinct int
if err := ix.db.QueryRow(`SELECT count(DISTINCT user) FROM event`).Scan(&distinct); err != nil {
t.Fatal(err)
}
c, err := ix.Counts(false)
if err != nil {
t.Fatal(err)
}
if c.Contributors != distinct {
t.Fatalf("census contributors=%d, DISTINCT user over events=%d", c.Contributors, distinct)
}
}
// The memo exists because the fleet home renders one row per
// repository; the fold that invalidates it is the same fold that makes
// it wrong. A fleet row that lags a push is the small lie the census
// memo was already careful about.
func TestSummaryRefreshesAfterAFold(t *testing.T) {
ix := openIndex(t)
if sm, err := ix.Summary(); err != nil || sm.Checkins != 0 {
t.Fatalf("empty index: %+v err=%v", sm, err)
}
if err := ix.FoldNow([]Parsed{checkinBy("c1", "dan", "2026-01-01T00:00:00")}); err != nil {
t.Fatal(err)
}
sm, err := ix.Summary()
if err != nil {
t.Fatal(err)
}
if sm.Checkins != 1 || sm.Contributors != 1 {
t.Fatalf("after fold %+v, want 1/1 — the memo went stale", sm)
}
if err := ix.FoldNow([]Parsed{checkinBy("c2", "eve", "2026-01-02T00:00:00")}); err != nil {
t.Fatal(err)
}
if sm, _ = ix.Summary(); sm.Checkins != 2 || sm.Contributors != 2 {
t.Fatalf("after second fold %+v, want 2/2", sm)
}
// Reset is the reindex path; neither memo may survive it.
if err := ix.Reset(); err != nil {
t.Fatal(err)
}
if sm, _ = ix.Summary(); sm.Checkins != 0 {
t.Fatalf("summary survived a reset: %+v", sm)
}
}
// A warm census must satisfy Summary without touching the database
// again — the repository page and the fleet row ask for overlapping
// work and whichever arrives first should pay for both.
func TestSummaryIsServedFromAWarmCensus(t *testing.T) {
ix := openIndex(t)
if err := ix.FoldNow([]Parsed{
checkinBy("c1", "dan", "2026-01-01T00:00:00"),
checkinBy("c2", "eve", "2026-01-02T00:00:00"),
}); err != nil {
t.Fatal(err)
}
if _, err := ix.Counts(false); err != nil {
t.Fatal(err)
}
// With the census warm and the narrow memo still empty, an answer
// can only have come from the census.
ix.countMu.Lock()
warm, narrow := ix.counts[privacyIdx(false)] != nil, ix.summary != nil
ix.countMu.Unlock()
if !warm || narrow {
t.Fatalf("precondition: census warm=%v narrow memo=%v", warm, narrow)
}
sm, err := ix.Summary()
if err != nil {
t.Fatal(err)
}
if sm.Checkins != 2 || sm.Contributors != 2 {
t.Fatalf("%+v, want 2/2", sm)
}
ix.countMu.Lock()
filled := ix.summary != nil
ix.countMu.Unlock()
if filled {
t.Error("Summary queried and memoised separately although the census was already warm")
}
}