Artifact 2b91b34d9e8fb4897e7f0d950396b57d33ff0273a883bf3941f9be4caab3d12b:
- File
internal/index/landingevent_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: 9315) [annotate] [blame] [check-ins using] [more...]
package index import ( "testing" ) // eventRow reads one event's kind and label. func eventRow(t *testing.T, ix *Index, hash string) (kind, ref, comment string) { t.Helper() if err := ix.db.QueryRow(`SELECT kind, ref, comment FROM event WHERE hash = ?`, hash).Scan(&kind, &ref, &comment); err != nil { t.Fatalf("no event for %s: %v", hash, err) } return } // Landings are built on forum artifacts, so the forum trigger filed // every comment and every verdict as kind 'f' with a "Forum: " prefix. // Three landings produced rows reading "Forum: zit-landing-state: // accepted Accepted. The forge now tells y" — a prefix over a marker // over a truncated body. func TestLandingPostsAreNotForumEvents(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "aaaa1111", "", "Ship the drilldown", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00", "just a remark") ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base") ci(t, ix, "f1", "feat/x", "2026-01-01T11:00:00", "work", "t1") ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1") foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-03T10:00:00", landingState("accepted", "landed")) kind, ref, comment := eventRow(t, ix, "aaaa1111") if kind != "l" { t.Errorf("the landing root is kind %q, want l", kind) } if ref != "aaaa1111" { t.Errorf("root ref %q, want the landing id so the row can link to it", ref) } if comment != "Landing: Ship the drilldown" { t.Errorf("root label %q", comment) } kind, ref, comment = eventRow(t, ix, "cccc3333") if kind != "l" { t.Errorf("a verdict is kind %q, want l — it changes the target branch", kind) } if comment != "Landing accepted: Ship the drilldown" { t.Errorf("verdict label %q", comment) } if ref != "aaaa1111" { t.Errorf("verdict ref %q, want the landing id", ref) } kind, _, comment = eventRow(t, ix, "bbbb2222") if kind != "lc" { t.Errorf("a landing comment is kind %q, want lc", kind) } if comment != "Comment on landing: Ship the drilldown" { t.Errorf("comment label %q", comment) } // Nothing in a landing may still read as forum content. var forum int if err := ix.db.QueryRow(`SELECT count(*) FROM event WHERE kind = 'f'`).Scan(&forum); err != nil { t.Fatal(err) } if forum != 0 { t.Errorf("%d landing posts still filed as forum events", forum) } } // A verdict is labelled by ITS OWN state, not the thread's final one: a // timeline row says what happened at that moment. func TestEachVerdictKeepsItsOwnState(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "aaaa1111", "", "Two minds", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00", landingState("rejected", "not yet")) ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base") ci(t, ix, "f1", "feat/x", "2026-01-01T11:00:00", "work", "t1") ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1") foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-03T10:00:00", landingState("accepted", "now")) if _, _, c := eventRow(t, ix, "bbbb2222"); c != "Landing rejected: Two minds" { t.Errorf("the earlier verdict reads %q — relabelled by how the story ended", c) } if _, _, c := eventRow(t, ix, "cccc3333"); c != "Landing accepted: Two minds" { t.Errorf("the settling verdict reads %q", c) } } // Ordinary forum threads must be untouched: the classification keys on // the landing header, not on being a thread. func TestOrdinaryForumPostsStayForumEvents(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "dddd4444", "", "How do I build this", "2026-01-01T10:00:00", "a real question") foldForumBody(t, ix, "eeee5555", "dddd4444", "", "2026-01-02T10:00:00", "an answer") for _, h := range []string{"dddd4444", "eeee5555"} { if k, _, _ := eventRow(t, ix, h); k != "f" { t.Errorf("forum post %s became kind %q", h, k) } } } // A reply folded before its root is the order-independence rule the // fold already keeps for the landing record itself; the events have to // converge the same way. func TestLandingEventsConvergeWhateverTheOrder(t *testing.T) { ix := openIndex(t) ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base") ci(t, ix, "f1", "feat/x", "2026-01-01T11:00:00", "work", "t1") ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1") foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-03T10:00:00", landingState("accepted", "landed")) // The reply arrived first: with no root there is no landing yet. if k, _, _ := eventRow(t, ix, "cccc3333"); k != "f" { t.Fatalf("precondition: orphan reply is kind %q, expected f", k) } foldForumBody(t, ix, "aaaa1111", "", "Late root", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) if k, _, c := eventRow(t, ix, "cccc3333"); k != "l" || c != "Landing accepted: Late root" { t.Errorf("after the root arrived the verdict is %q/%q — it did not converge", k, c) } } // The classification moved out of the trigger and into the fold, so a // store folded before that change carries "Forum:" rows for landings it // already holds. Derived data that is only correct after a manual // reindex is a chore, not derived data. func TestBackfillRefilesLandingEventsInAnUpgradedIndex(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "aaaa1111", "", "Older store", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base") ci(t, ix, "f1", "feat/x", "2026-01-01T11:00:00", "work", "t1") ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1") foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-03T10:00:00", landingState("accepted", "landed")) // Simulate the old shape: the trigger's classification, unfixed. if _, err := ix.db.Exec(`UPDATE event SET kind = 'f', comment = 'Forum: stale' WHERE hash IN ('aaaa1111', 'cccc3333')`); err != nil { t.Fatal(err) } if err := ix.backfillLandingEvents(); err != nil { t.Fatal(err) } if k, _, c := eventRow(t, ix, "aaaa1111"); k != "l" || c != "Landing: Older store" { t.Errorf("root not refiled: %q/%q", k, c) } if k, _, c := eventRow(t, ix, "cccc3333"); k != "l" || c != "Landing accepted: Older store" { t.Errorf("verdict not refiled: %q/%q", k, c) } // Idempotent: a store with nothing to fix must not be rewritten. if _, err := ix.db.Exec(`UPDATE event SET comment = 'sentinel' WHERE hash = 'aaaa1111'`); err != nil { t.Fatal(err) } if err := ix.backfillLandingEvents(); err != nil { t.Fatal(err) } if _, _, c := eventRow(t, ix, "aaaa1111"); c != "sentinel" { t.Error("backfill rewrote a store that had nothing stale in it") } } // kindsIn runs the timeline with a filter and reports the kinds it returned. func kindsIn(t *testing.T, ix *Index, y string) map[string]int { t.Helper() es, _, err := ix.Timeline(TimelineFilter{Kind: y, Limit: 50}) if err != nil { t.Fatal(err) } got := map[string]int{} for _, e := range es { got[e.Kind]++ } return got } // A verdict changes the target branch and belongs in a mixed timeline. // A landing comment is conversation — three landings produced six rows // of it — so it shows only where someone asked for landings. func TestMixedTimelineCarriesVerdictsButNotChatter(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "aaaa1111", "", "Ship it", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00", "a remark") ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base") ci(t, ix, "f1", "feat/x", "2026-01-01T11:00:00", "work", "t1") ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1") foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-03T10:00:00", landingState("accepted", "landed")) foldForumBody(t, ix, "dddd4444", "", "A real thread", "2026-01-04T10:00:00", "hello") all := kindsIn(t, ix, "all") if all["lc"] != 0 { t.Errorf("y=all carried %d landing comments", all["lc"]) } if all["l"] != 2 { t.Errorf("y=all carried %d landing rows, want the root and the verdict", all["l"]) } if all["f"] != 1 { t.Errorf("y=all carried %d forum rows, want the one real thread", all["f"]) } // The landing view is the whole record, conversation included. l := kindsIn(t, ix, "l") if l["l"] != 2 || l["lc"] != 1 { t.Errorf("y=l gave %v, want 2 landing rows and 1 comment", l) } if l["f"] != 0 { t.Error("y=l carried ordinary forum posts") } // ...and the forum view is unaffected by any of it. if f := kindsIn(t, ix, "f"); f["f"] != 1 || len(f) != 1 { t.Errorf("y=f gave %v, want only the one real thread", f) } } // An unfiltered call is what other surfaces use; chatter must not leak // into those either. func TestUnfilteredTimelineAlsoOmitsLandingChatter(t *testing.T) { ix := openIndex(t) foldForumBody(t, ix, "aaaa1111", "", "Ship it", "2026-01-01T10:00:00", landingBody("feat/x", "trunk", "ready")) foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00", "a remark") if got := kindsIn(t, ix, ""); got["lc"] != 0 { t.Errorf("an unfiltered timeline carried %d landing comments", got["lc"]) } }