Skip to content
quarry Artifact [7c08541afc]
Login

Artifact [7c08541afc]

Artifact 7c08541afca54e706b0e6a4feb4cf2d45fca2ac949e001cd6ab445c4f6e03025:


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, "", false)
	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, "", true)
	if err != nil {
		t.Fatal(err)
	}
	if len(all) != 5 {
		t.Errorf("opting in returned %d tickets, want all 5", len(all))
	}
}

// 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, "", true)
	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)
	}
}