package harness
import (
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// Leaf status is repo-state: the repo-state RFC defines it and zit
// derives and caches it. quarry must not re-derive it.
//
// Its own predicate answered "no check-in lists this one as a parent" —
// which §7.2 names explicitly as NOT the canonical test. The canonical
// one is "no child ON THE SAME BRANCH", and open leaves additionally
// exclude an active closed tag. On the fossil corpus that was 490
// marked against a true 1432, and a different set: a forge stating
// false history about a store that is entirely correct.
//
// The fixture branches deliberately. On a linear history the two
// definitions agree and this test would pass while the bug stood.
func TestLeafMarksAgreeWithZit(t *testing.T) {
repo := buildStore(t, "leafparity")
if err := os.WriteFile(filepath.Join(repo, "side.txt"), []byte("side\n"), 0o644); err != nil {
t.Fatal(err)
}
zrun(t, repo, "add", "side.txt")
// --branch starts a new branch AT this check-in, so trunk's previous
// tip keeps a child that is not on trunk: exactly the shape where
// "no child at all" and "no same-branch child" disagree.
zrun(t, repo, "commit", "-m", "work on the side branch", "--branch", "side")
srv := ingestAndServe(t, map[string]string{"alpha": repo}, t.TempDir())
// zit's answer, which is the definition.
out, err := exec.Command("zit", "leaves", "-R", repo).Output()
if err != nil {
t.Fatalf("zit leaves: %v", err)
}
// zit ≤0.2 printed "* <hash> <branch>"; 0.3 prints Fossil's
// timeline line with a twelve-character bracketed hash. Collect
// prefixes and compare prefix-wise — the definition is zit's
// either way.
want := map[string]bool{}
for _, line := range strings.Split(string(out), "\n") {
s := strings.TrimSpace(line)
if f := strings.Fields(strings.TrimPrefix(s, "* ")); len(f) > 0 && len(f[0]) >= 40 {
want[f[0][:12]] = true
continue
}
if o, c := strings.IndexByte(s, '['), strings.IndexByte(s, ']'); o >= 0 && c > o+1 {
want[s[o+1:c]] = true
}
}
if len(want) == 0 {
t.Fatal("zit reported no leaves for the fixture")
}
resp, err := srv.Client().Get(srv.URL + "/api/v1/r/alpha/timeline?limit=200")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var tl struct {
Entries []struct {
Kind string `json:"kind"`
Hash string `json:"hash"`
IsLeaf bool `json:"isLeaf"`
} `json:"entries"`
}
if err := json.Unmarshal(body, &tl); err != nil {
t.Fatal(err)
}
checkins, marked, disagree := 0, 0, 0
for _, e := range tl.Entries {
if e.Kind != "ci" {
continue
}
checkins++
if e.IsLeaf {
marked++
}
if e.IsLeaf != want[e.Hash[:12]] {
disagree++
t.Errorf("check-in %s: quarry says leaf=%v, zit says leaf=%v",
e.Hash[:12], e.IsLeaf, want[e.Hash[:12]])
}
}
if checkins == 0 {
t.Fatal("no check-ins on the timeline")
}
if disagree == 0 {
t.Logf("%d check-ins, %d marked leaf, agreeing with zit's %d", checkins, marked, len(want))
}
}