package index
import (
"testing"
)
// ci records a check-in with an explicit parent chain.
func ci(t *testing.T, ix *Index, hash, branch, date, comment string, parents ...string) {
t.Helper()
if _, err := ix.db.Exec(`INSERT OR REPLACE INTO checkin (hash, date, user, comment, branch)
VALUES (?, ?, 'dan', ?, ?)`, hash, date, comment, branch); err != nil {
t.Fatal(err)
}
for i, p := range parents {
if _, err := ix.db.Exec(`INSERT OR REPLACE INTO checkin_parent (child, parent, ord)
VALUES (?, ?, ?)`, hash, p, i); err != nil {
t.Fatal(err)
}
}
}
func order(cs []BranchCommit) []string {
out := make([]string, len(cs))
for i, c := range cs {
out[i] = c.Hash
}
return out
}
// The test that proves it, and the reason zit caught this while review
// did not: two check-ins in the SAME SECOND tie on date and then order
// by hash, which has nothing to do with which is the parent.
//
// The hashes here are chosen so that hash order is the REVERSE of
// ancestry order — a date sort with a hash tiebreak gets this exactly
// backwards, and looks entirely plausible doing it.
func TestBranchCommitsOrderByAncestryNotDate(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "zzz1", "trunk", "2026-01-01T10:00:00", "base")
ci(t, ix, "ccc1", "feat", "2026-01-02T10:00:00", "first on the branch", "zzz1")
ci(t, ix, "bbb2", "feat", "2026-01-02T10:00:00", "second, same second", "ccc1")
ci(t, ix, "aaa3", "feat", "2026-01-02T10:00:00", "third, same second", "bbb2")
got, err := ix.BranchCommits("feat", 50)
if err != nil {
t.Fatal(err)
}
want := []string{"ccc1", "bbb2", "aaa3"}
have := order(got)
if len(have) != len(want) {
t.Fatalf("got %v, want %v", have, want)
}
for i := range want {
if have[i] != want[i] {
t.Fatalf("ancestry order is %v, want %v — sorted by something\n"+
"other than the DAG", have, want)
}
}
}
// A parent must precede its child. That is the whole invariant, and it
// has to hold whatever the dates say — including when a clock went
// backwards between two commits.
func TestBranchCommitsKeepParentsFirstAcrossNonMonotoneDates(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "zzz1", "trunk", "2026-01-01T10:00:00", "base")
ci(t, ix, "p", "feat", "2026-01-05T10:00:00", "parent, later clock", "zzz1")
ci(t, ix, "c", "feat", "2026-01-02T10:00:00", "child, earlier clock", "p")
got, _ := ix.BranchCommits("feat", 50)
have := order(got)
if len(have) != 2 || have[0] != "p" || have[1] != "c" {
t.Fatalf("got %v, want [p c] — a child sorted ahead of its parent", have)
}
}
// The walk stops where the branch does: shared history is the target's,
// not the proposal's.
func TestBranchCommitsExcludeSharedHistory(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T10:00:00", "trunk one")
ci(t, ix, "t2", "trunk", "2026-01-01T11:00:00", "trunk two", "t1")
ci(t, ix, "f1", "feat", "2026-01-02T10:00:00", "branch one", "t2")
got, _ := ix.BranchCommits("feat", 50)
if have := order(got); len(have) != 1 || have[0] != "f1" {
t.Fatalf("got %v, want only the branch's own commit", have)
}
}
// A branch that forked and rejoined: the merge's depth must be the
// LONGEST path back, or a commit on the short side outranks a child it
// is an ancestor of.
func TestBranchCommitsHandleAForkAndRejoin(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T10:00:00", "base")
ci(t, ix, "f1", "feat", "2026-01-02T10:00:00", "root", "t1")
ci(t, ix, "f2", "feat", "2026-01-02T10:00:00", "left", "f1")
ci(t, ix, "f3", "feat", "2026-01-02T10:00:00", "left again", "f2")
ci(t, ix, "f4", "feat", "2026-01-02T10:00:00", "right", "f1")
ci(t, ix, "f5", "feat", "2026-01-02T10:00:00", "rejoin", "f3", "f4")
got, _ := ix.BranchCommits("feat", 50)
pos := map[string]int{}
for i, c := range got {
pos[c.Hash] = i
}
if len(pos) != 5 {
t.Fatalf("got %d commits, want 5: %v", len(pos), order(got))
}
for _, pair := range [][2]string{{"f1", "f2"}, {"f2", "f3"}, {"f1", "f4"},
{"f3", "f5"}, {"f4", "f5"}} {
if pos[pair[0]] >= pos[pair[1]] {
t.Errorf("%s does not precede %s: %v", pair[0], pair[1], order(got))
}
}
}
// An empty or unknown branch is not an error; a landing can name a
// branch whose check-ins have not been folded yet.
func TestBranchCommitsToleratesAnUnknownBranch(t *testing.T) {
ix := openIndex(t)
got, err := ix.BranchCommits("nothing-here", 50)
if err != nil || len(got) != 0 {
t.Fatalf("got %v err=%v", got, err)
}
if got, err := ix.BranchCommits("", 50); err != nil || got != nil {
t.Fatalf("empty branch: %v err=%v", got, err)
}
}
// The landing page lists a branch's check-ins through here, and an
// amended comment (a +comment tag) is the comment a reader is shown,
// as on the timeline. The stored C card stays as it was.
func TestBranchCommitsShowAnAmendedComment(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "zzz1", "trunk", "2026-01-01T10:00:00", "base")
ci(t, ix, "ccc1", "feat", "2026-01-02T10:00:00", "fixs the thing", "zzz1")
ci(t, ix, "bbb2", "feat", "2026-01-02T11:00:00", "untouched", "ccc1")
if _, err := ix.db.Exec(`INSERT INTO checkin_tag (src, date, user, checkin, name, sign, value)
VALUES ('ddd4', '2026-01-03T10:00:00', 'dan', 'ccc1', 'comment', '+', 'fixes the thing')`); err != nil {
t.Fatal(err)
}
got, err := ix.BranchCommits("feat", 50)
if err != nil {
t.Fatal(err)
}
want := map[string]string{"ccc1": "fixes the thing", "bbb2": "untouched"}
if len(got) != len(want) {
t.Fatalf("got %d check-ins, want %d", len(got), len(want))
}
for _, c := range got {
if c.Comment != want[c.Hash] {
t.Errorf("%s shows %q, want %q", c.Hash, c.Comment, want[c.Hash])
}
}
}