Skip to content
quarry Artifact [ba4be95649]
Login

Artifact [ba4be95649]

Artifact ba4be9564986959e32cb60f5ebfd419cb429cc8630da9a12a3d512787888b83e:


package api

import (
	"context"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

// forumWith folds one thread per title so each is findable by a word.
func forumWith(t *testing.T, titles ...string) *Repo {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "i.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	var batch []index.Parsed
	for i, title := range titles {
		h := strings.Repeat("0", 61) + string("0123456789abcdef"[i%16]) + "ab"
		batch = append(batch, index.Parsed{Hash: h, Art: &artifact.Artifact{
			Type: artifact.Forum, ThreadTitle: title, User: "dan",
			Date: "2026-09-01T10:00:00", Body: []byte(title + " — body text"),
		}})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	return &Repo{Index: ix}
}

func searchForum(t *testing.T, d Deps, repo, q string) []index.SearchResult {
	t.Helper()
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	got, err := r.Invoke(context.Background(), "search.query",
		searchIn{Repo: repo, Q: q, Kind: "forum", Limit: 50})
	if err != nil {
		t.Fatal(err)
	}
	return got.(searchOut).Results
}

// Searching a bound repository's forum must reach the store that holds
// it. On fossil this answered "nothing matches" for a word in 3,558
// threads: the parent's index has never heard of them, and the store
// that could answer was never asked (RFC 0015 §Search).
func TestSearchingAForumReachesTheCompanion(t *testing.T) {
	d := Deps{Version: "v",
		Repos: NewRepos(map[string]*Repo{
			"fossil":       forumWith(t, "a legacy thread about nothing"),
			"fossil-forum": forumWith(t, "the difference between two things"),
		}),
		Companions: &fakeCompanions{bound: map[string]string{"fossil": "fossil-forum"}},
	}
	got := searchForum(t, d, "fossil", "difference")
	if len(got) == 0 {
		t.Fatal("FAIL: a word that exists in the companion found nothing")
	}
	found := false
	for _, r := range got {
		if strings.Contains(r.Label+r.Snippet, "difference") {
			found = true
		}
	}
	if !found {
		t.Errorf("FAIL: the companion's thread is not among the results: %+v", got)
	}
}

// Merged, not replaced: a repository bound after it had threads of its
// own still has them, and both are on the forum page a reader clicks
// through to.
func TestTheParentsOwnThreadsAreStillFound(t *testing.T) {
	d := Deps{Version: "v",
		Repos: NewRepos(map[string]*Repo{
			"fossil":       forumWith(t, "a legacy thread about pelicans"),
			"fossil-forum": forumWith(t, "a newer thread about pelicans"),
		}),
		Companions: &fakeCompanions{bound: map[string]string{"fossil": "fossil-forum"}},
	}
	got := searchForum(t, d, "fossil", "pelicans")
	if len(got) != 2 {
		t.Fatalf("FAIL: found %d threads, want both the parent's and the companion's", len(got))
	}
}

// An unbound repository behaves exactly as before, and a companion is
// not searchable under its own name for things it does not hold.
func TestAnUnboundRepositorySearchesOnlyItself(t *testing.T) {
	d := Deps{Version: "v",
		Repos: NewRepos(map[string]*Repo{
			"alpha": forumWith(t, "alpha's own thread about pelicans"),
			"beta":  forumWith(t, "beta's thread about pelicans"),
		}),
	}
	if got := searchForum(t, d, "alpha", "pelicans"); len(got) != 1 {
		t.Errorf("FAIL: an unbound repository returned %d results, want only its own", len(got))
	}
}

// A binding naming a store this node does not serve is an operator's
// problem, worded properly the moment somebody opens the forum. A
// search says what it found rather than failing on it.
func TestADanglingBindingStillSearchesTheParent(t *testing.T) {
	d := Deps{Version: "v",
		Repos:      NewRepos(map[string]*Repo{"fossil": forumWith(t, "a thread about pelicans")}),
		Companions: &fakeCompanions{bound: map[string]string{"fossil": "nowhere"}},
	}
	got := searchForum(t, d, "fossil", "pelicans")
	if len(got) != 1 {
		t.Errorf("FAIL: a dangling binding gave %d results, want the parent's own", len(got))
	}
}

// The merge is deterministic: equal ranks break on ref, so two runs of
// the same query cannot reorder under a reader.
func TestTheMergeIsDeterministic(t *testing.T) {
	a := []index.SearchResult{{Ref: "bbb", Rank: -1}, {Ref: "aaa", Rank: -1}}
	b := []index.SearchResult{{Ref: "ccc", Rank: -1}}
	first := mergeSearch(a, b, 0)
	second := mergeSearch(a, b, 0)
	for i := range first {
		if first[i].Ref != second[i].Ref {
			t.Fatalf("FAIL: the merge reordered between runs: %v vs %v", first, second)
		}
	}
	if first[0].Ref != "aaa" {
		t.Errorf("FAIL: ties did not break on ref: %v", first)
	}
	// A better rank still wins regardless of which side it came from.
	if got := mergeSearch(
		[]index.SearchResult{{Ref: "worse", Rank: -1}},
		[]index.SearchResult{{Ref: "better", Rank: -9}}, 0); got[0].Ref != "better" {
		t.Errorf("FAIL: the companion's stronger hit did not lead: %v", got)
	}
	// And the limit is honoured after the merge, not before it.
	if got := mergeSearch(a, b, 2); len(got) != 2 {
		t.Errorf("FAIL: limit gave %d", len(got))
	}
}

// Fleet-wide, a companion's hits are the parent's: "the companion is
// not searchable on its own name" (RFC 0015 §Search), and a reader who
// clicks a forum result lands on /r/X/forum whichever store held it.
func TestFleetSearchAttributesACompanionToItsParent(t *testing.T) {
	d := Deps{Version: "v",
		Repos: NewRepos(map[string]*Repo{
			"fossil":       forumWith(t, "a legacy thread about pelicans"),
			"fossil-forum": forumWith(t, "a newer thread about pelicans"),
			"sqlite":       forumWith(t, "sqlite on pelicans"),
		}),
		Companions: &fakeCompanions{bound: map[string]string{"fossil": "fossil-forum"}},
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	got, err := r.Invoke(context.Background(), "search.all",
		searchAllIn{Q: "pelicans", Kind: "forum", Limit: 50})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(searchAllOut)
	for _, h := range out.Results {
		if h.Repo == "fossil-forum" {
			t.Errorf("FAIL: a hit is attributed to the companion: %+v", h)
		}
	}
	// One facet per repository, with the companion's hits folded in:
	// two rows for fossil would say one project is two.
	seen := map[string]int{}
	for _, f := range out.ByRepo {
		seen[f.Repo]++
	}
	if seen["fossil-forum"] != 0 {
		t.Error("FAIL: the companion is listed as a repository of its own")
	}
	if seen["fossil"] != 1 {
		t.Errorf("FAIL: fossil appears in %d facets, want one: %+v", seen["fossil"], out.ByRepo)
	}
	// Both of fossil's threads are counted under it.
	for _, f := range out.ByRepo {
		if f.Repo == "fossil" && f.Count != 2 {
			t.Errorf("FAIL: fossil's facet counts %d, want both its threads", f.Count)
		}
	}
}