Skip to content
quarry Artifact [58d7ed88d1]
Login

Artifact [58d7ed88d1]

Artifact 58d7ed88d1cc3d0165d05eea075332a61cbbd054af86422c206e7ddcc1810ce3:


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 != 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")
	}
}