package index
import (
"fmt"
"path/filepath"
"sort"
"strings"
"testing"
"craftdesign.group/quarry/internal/artifact"
)
func edgeHash(n int) string { return fmt.Sprintf("%040x", n) }
func edgeCi(n, parent int, date string, tags ...artifact.Tag) Parsed {
a := &artifact.Artifact{Type: artifact.Manifest, Date: date, User: "u", Comment: fmt.Sprint(n), Tags: tags}
if parent > 0 {
a.Parents = []string{edgeHash(parent)}
}
return Parsed{Hash: edgeHash(n), Art: a}
}
func edgeState(t *testing.T, ix *Index) string {
t.Helper()
var out []string
rows, err := ix.db.Query(`SELECT hash, branch, COALESCE((SELECT group_concat(name) FROM (
SELECT name FROM checkin_sym s WHERE s.checkin = c.hash ORDER BY name)), '') FROM checkin c`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var h, b, s string
rows.Scan(&h, &b, &s)
out = append(out, h[36:]+"="+b+"/"+s)
}
sort.Strings(out)
return strings.Join(out, " ")
}
// #263: a round that only grows the graph at its edge computes the new
// check-ins from their parents, and that answer is the wholesale walk's;
// a round that cannot (a parent missing, a tag re-aiming an old
// check-in) walks the whole graph.
func TestBranchesGrownAtTheEdgeMatchTheWholeWalk(t *testing.T) {
open := func() *Index {
ix, err := Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
return ix
}
tag := func(name, value string) artifact.Tag { return artifact.Tag{Name: name, Target: "*", Value: value} }
base := []Parsed{
edgeCi(1, 0, "2026-01-01T00:00:00", tag("*branch", "trunk"), tag("*sym-trunk", "")),
edgeCi(2, 1, "2026-01-02T00:00:00"),
edgeCi(3, 2, "2026-01-03T00:00:00", tag("*branch", "feature"), tag("*sym-feature", ""), tag("-sym-trunk", "")),
}
rounds := [][]Parsed{
{edgeCi(4, 3, "2026-01-04T00:00:00")}, // inherits feature
{edgeCi(5, 2, "2026-01-05T00:00:00"), edgeCi(6, 5, "2026-01-06T00:00:00")}, // a chain on trunk in one batch
{edgeCi(7, 4, "2026-01-07T00:00:00", tag("*sym-release", ""))}, // a new name on the branch
{edgeCi(9, 8, "2026-01-09T00:00:00")}, // its parent has not arrived: whole walk
{edgeCi(8, 6, "2026-01-08T00:00:00")}, // the parent arrives under its child: whole walk
{{Hash: edgeHash(10), Art: &artifact.Artifact{Type: artifact.Control, Date: "2026-01-10T00:00:00", User: "u",
Tags: []artifact.Tag{{Name: "*branch", Target: edgeHash(6), Value: "moved"}}}}}, // re-aims an old check-in: whole walk
}
edge := open()
if err := edge.FoldNow(base); err != nil {
t.Fatal(err)
}
whole := open()
all := append([]Parsed{}, base...)
for i, r := range rounds {
if err := edge.FoldNow(r); err != nil {
t.Fatal(err)
}
all = append(all, r...)
if _, err := whole.Fold(all); err != nil {
t.Fatal(err)
}
if err := whole.RecomputeBranches(); err != nil {
t.Fatal(err)
}
if got, want := edgeState(t, edge), edgeState(t, whole); got != want {
t.Fatalf("FAIL: after round %d the edge path reads\n %s\nthe whole walk\n %s", i+1, got, want)
}
}
if s := edgeState(t, edge); !strings.Contains(s, edgeHash(7)[36:]+"=feature/feature,release") {
t.Errorf("FAIL: the new name did not join the inherited one: %s", s)
}
}