package index
import (
"testing"
)
// foldMerge records a check-in whose second parent is a branch tip —
// the shape zit writes when it lands a branch.
func foldMerge(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 (?, ?, 'dmestas', ?, ?)`, 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)
}
}
}
// landed builds an accepted landing with a branch tip and its merge.
func landed(t *testing.T, ix *Index, root, branch, title string) {
t.Helper()
foldForumBody(t, ix, root, "", title, "2026-01-01T10:00:00",
landingBody(branch, "trunk", "please"))
// The DAG first: these rows are written directly, not folded, so
// nothing re-derives the record after them. The reply that follows
// re-folds the thread and finds the merge already there — which is
// also the order the forge sees when zit accepts.
foldMerge(t, ix, "tip"+branch, branch, "2026-01-02T10:00:00", "work on "+branch)
foldMerge(t, ix, "mrg"+branch, "trunk", "2026-01-02T12:00:00",
"land "+branch, "prevtrunk", "tip"+branch)
foldForumBody(t, ix, root+"ff", root, "", "2026-01-03T10:00:00",
landingState("accepted", "landed"))
}
// landing.settled_by holds the reply that DECIDED the state — a forum
// post hash, not a commit — which was read as meaning nothing connects
// a landing to the check-in it produced. The answer was already in the
// DAG: a merge check-in lists its primary parent first and the branches
// it merged after, so ord > 0 IS the tip that landed.
func TestLandingResolvesToItsMergeCheckin(t *testing.T) {
ix := openIndex(t)
landed(t, ix, "aaaa1111", "feat-x", "Ship it")
got, err := ix.MergesForLandings([]string{"aaaa1111"})
if err != nil {
t.Fatal(err)
}
m, ok := got["aaaa1111"]
if !ok {
t.Fatal("an accepted landing resolved to no check-in")
}
if m.Checkin != "mrgfeat-x" {
t.Errorf("resolved to %q, want the merge on trunk", m.Checkin)
}
if m.Comment != "land feat-x" {
t.Errorf("comment %q", m.Comment)
}
}
// The merge lands BEFORE the artifact recording the verdict is written
// — one landing in this forge merged at 12:46:55 and was recorded at
// 12:49:51 — so bounding the search on the verdict silently loses
// exactly the check-ins this exists to find. A proposal cannot be
// landed before it is made; that is the bound that holds.
func TestMergeIsFoundEvenWhenItPrecedesTheVerdict(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "Ship it", "2026-01-01T10:00:00",
landingBody("feat-x", "trunk", "please"))
foldMerge(t, ix, "tipfeat-x", "feat-x", "2026-01-02T12:46:55", "work")
foldMerge(t, ix, "mrgfeat-x", "trunk", "2026-01-02T12:46:55", "land it",
"prevtrunk", "tipfeat-x")
// The verdict is filed three minutes AFTER the merge.
foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-02T12:49:51",
landingState("accepted", "landed"))
got, err := ix.MergesForLandings([]string{"aaaa1111"})
if err != nil {
t.Fatal(err)
}
if _, ok := got["aaaa1111"]; !ok {
t.Fatal("the merge was lost because it preceded its own verdict record")
}
}
// Open and rejected landings produced nothing, and must not borrow a
// merge that happens to touch the same branch.
// A landing nothing merged resolves to nothing, open or rejected. And
// the case this test used to assert the other way: a merge of the
// branch onto the target after the landing opened IS the landing's
// merge, whoever made it and whatever the thread says — RFC 0007's
// "the merge is the fact". The record settles once something re-reads
// it, which is what the refresh after every branch recompute does.
func TestUnsettledLandingsResolveToNothing(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "Still open", "2026-01-01T10:00:00",
landingBody("feat-x", "trunk", "please"))
if got, err := ix.MergesForLandings([]string{"aaaa1111"}); err != nil || len(got) != 0 {
t.Fatalf("an OPEN landing with no merge resolved to %v (err=%v)", got, err)
}
foldForumBody(t, ix, "bbbb2222", "", "Not this", "2026-01-01T10:00:00",
landingBody("feat-y", "trunk", "please"))
foldForumBody(t, ix, "cccc3333", "bbbb2222", "", "2026-01-03T10:00:00",
landingState("rejected", "no"))
if got, _ := ix.MergesForLandings([]string{"bbbb2222"}); len(got) != 0 {
t.Errorf("a REJECTED landing with no merge resolved to %v", got)
}
// Merged by hand, from a clone, no reply: still landed.
foldMerge(t, ix, "tipfeat-x", "feat-x", "2026-01-02T10:00:00", "work")
foldMerge(t, ix, "mrgfeat-x", "trunk", "2026-01-02T12:00:00", "merged by hand",
"prevtrunk", "tipfeat-x")
tx, err := ix.db.Begin()
if err != nil {
t.Fatal(err)
}
if err := refreshLandingStates(tx); err != nil {
t.Fatal(err)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
got, _ := ix.MergesForLandings([]string{"aaaa1111"})
if m, ok := got["aaaa1111"]; !ok || m.Checkin != "mrgfeat-x" {
t.Errorf("a by-hand merge after opening did not settle the landing: %v", got)
}
}
// A merge somebody made by hand carries no landing, and the timeline
// decoration's absence is what distinguishes the two.
func TestTimelineDecoratesOnlyLandingMerges(t *testing.T) {
ix := openIndex(t)
landed(t, ix, "aaaa1111", "feat-x", "Ship it")
// An unrelated merge of a branch no landing proposed.
foldMerge(t, ix, "tiphotfix", "hotfix", "2026-01-04T10:00:00", "urgent")
foldMerge(t, ix, "mrghotfix", "trunk", "2026-01-04T12:00:00", "merge hotfix",
"prevtrunk", "tiphotfix")
rows := []TimelineEntry{
{Kind: "ci", Hash: "mrgfeat-x", IsMerge: true},
{Kind: "ci", Hash: "mrghotfix", IsMerge: true},
{Kind: "ci", Hash: "tipfeat-x"},
}
if err := ix.attachLandings(rows); err != nil {
t.Fatal(err)
}
if rows[0].Landing != "aaaa1111" || rows[0].LandingTitle != "Ship it" {
t.Errorf("landing merge not decorated: %+v", rows[0])
}
if rows[0].LandingApprover != "dan" {
t.Errorf("approver %q, want the author of the verdict", rows[0].LandingApprover)
}
if rows[1].Landing != "" {
t.Errorf("a hand-made merge was decorated with %q", rows[1].Landing)
}
if rows[2].Landing != "" {
t.Error("a non-merge check-in was decorated")
}
}
// A check-in names the landing that produced it: the merge an accept
// wrote, and each check-in on the branch the landing carried. A hand
// merge and a check-in on the target belong to no landing.
func TestCheckinNamesItsLanding(t *testing.T) {
ix := openIndex(t)
landed(t, ix, "aaaa1111", "feat-x", "Ship it")
foldMerge(t, ix, "tiphotfix", "hotfix", "2026-01-04T10:00:00", "urgent")
foldMerge(t, ix, "mrghotfix", "trunk", "2026-01-04T12:00:00", "merge hotfix",
"prevtrunk", "tiphotfix")
foldMerge(t, ix, "plaintrunk", "trunk", "2026-01-05T12:00:00", "plain", "mrgfeat-x")
for hash, want := range map[string]LandedBy{
"mrgfeat-x": {ID: "aaaa1111", Title: "Ship it", State: "accepted", Role: "merge"},
"tipfeat-x": {ID: "aaaa1111", Title: "Ship it", State: "accepted", Role: "branch"},
"mrghotfix": {}, "tiphotfix": {}, "plaintrunk": {},
} {
got, err := ix.LandedByCheckin(hash)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("%s: got %+v, want %+v", hash, got, want)
}
}
}