Artifact 78f22222ebf476efc537f74009984577ab9e5a002dec614adccb79052b949557:
- File
internal/index/landingmerge_test.go
— part of check-in
[4832508615]
at
2026-08-26 23:13:58
on branch protect-3
— RFC 0007 phase 3: the merge is the fact; accept asks MayJoin
The fold reads accepted from the DAG, for every landing: the earliest target check-in after opening whose non-primary parent is on the proposed branch. A reply asserting acceptance with nothing merged leaves the record open and is recorded as a claim; the page says so, the timeline files it as conversation. Rejection has no DAG fact and stays the reply's to say. Re-derived after every branch recompute — which now also invalidates the count memo, since it can settle a landing — so a merge pushed from a clone with no reply settles.
landing.accept refuses a non-owner on a protected target before writing, naming the owners. landing.get reports MayLand beside CanAccept so the page shows a disabled control with the reason.
Fourteen existing tests settled records by reply alone, or asserted a by-hand merge does not count; each now carries the merge it announces or asserts the RFC's reading. Giving the web fixture real check-ins exposed checkin.diff dereferencing a nil store; it now refuses, after the lookups so not-found still wins. (user: dmestas, size: 6441) [annotate] [blame] [check-ins using] [more...]
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") } }