package index
import (
"testing"
)
// A landing record's root body, as zit mints it (zit#127).
func landingBody(branch, target, msg string) string {
return "zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\n" + msg + "\n"
}
func landingState(state, msg string) string {
return "zit-landing-state: " + state + "\n\n" + msg + "\n"
}
func onlyLanding(t *testing.T, ix *Index, state string) LandingSummary {
t.Helper()
got, err := ix.Landings(state, 0)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("Landings(%q) returned %d rows, want 1: %+v", state, len(got), got)
}
return got[0]
}
func TestLandingRootFoldsIntoARecord(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "land the x work", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready for review"))
l := onlyLanding(t, ix, "")
if l.Branch != "feat/x" || l.Target != "trunk" {
t.Fatalf("branch/target = %q/%q, want feat/x/trunk", l.Branch, l.Target)
}
if l.State != "open" {
t.Fatalf("state = %q, want open — a record with no transition is open by absence", l.State)
}
if l.Title != "land the x work" || l.Author != "dan" {
t.Fatalf("title/author = %q/%q", l.Title, l.Author)
}
if l.SettledBy != "" {
t.Fatalf("settled_by = %q, want empty while open", l.SettledBy)
}
}
func TestLandingStateIsLatestTransitionWins(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "land it", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready"))
// A plain comment must not settle anything.
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-01T11:00:00", "looks good\n")
if l := onlyLanding(t, ix, ""); l.State != "open" {
t.Fatalf("a comment settled the record: state = %q", l.State)
}
foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("rejected", "not yet"))
if l := onlyLanding(t, ix, ""); l.State != "rejected" || l.SettledBy != "cccc3333" {
t.Fatalf("state/settled_by = %q/%q, want rejected/cccc3333", l.State, l.SettledBy)
}
// Later wins, so a reopened-then-accepted thread reads accepted.
// RFC 0007: the merge is the fact. The accept reply settles the
// record because the DAG holds the merge it announces.
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
ci(t, ix, "f1", "feat/x", "2026-01-01T09:30:00", "work", "t1")
ci(t, ix, "m1", "trunk", "2026-01-03T09:59:00", "land feat/x", "t1", "f1")
foldForumBody(t, ix, "dddd4444", "aaaa1111", "", "2026-01-03T10:00:00",
landingState("accepted", "landed"))
l := onlyLanding(t, ix, "")
if l.State != "accepted" || l.SettledBy != "dddd4444" {
t.Fatalf("state/settled_by = %q/%q, want accepted/dddd4444", l.State, l.SettledBy)
}
if l.Updated != "2026-01-03T10:00:00" {
t.Fatalf("updated = %q, want the newest post's date", l.Updated)
}
if l.Opened != "2026-01-01T10:00:00" {
t.Fatalf("opened = %q, want the root's date", l.Opened)
}
}
// The fold must not depend on arrival order: sync delivers artifacts in
// store order, which is not thread order, and a reply can be folded before
// the root it belongs to.
func TestLandingFoldIsOrderIndependent(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-01T09:30:00", "work", "t1")
ci(t, ix, "m1", "trunk", "2026-01-02T09:59:00", "land feat/x", "t1", "f1")
foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("accepted", "landed"))
// The root has not arrived; nothing can be known yet, and that must not
// be an error or a half-written row.
if got, err := ix.Landings("", 0); err != nil || len(got) != 0 {
t.Fatalf("Landings before the root: %d rows, err=%v — want 0, nil", len(got), err)
}
foldForumBody(t, ix, "aaaa1111", "", "land it", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready"))
l := onlyLanding(t, ix, "")
if l.State != "accepted" || l.SettledBy != "cccc3333" {
t.Fatalf("out-of-order fold converged on %q/%q, want accepted/cccc3333", l.State, l.SettledBy)
}
}
func TestOrdinaryForumThreadIsNotALanding(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "Broken build", "2026-01-01T10:00:00",
"the build is broken on aarch64\n")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-01T11:00:00", "fixed\n")
got, err := ix.Landings("", 0)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("an ordinary thread indexed as %d landing(s): %+v", len(got), got)
}
}
// Refusing to guess: a header line this code does not know means the body
// speaks a dialect we only think we understand, so it is not read at all.
func TestLandingWithUnknownHeaderLineIsRefused(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "odd", "2026-01-01T10:00:00",
"zit-landing: 1\nbranch: feat/x\ntarget: trunk\nreviewers: someone\n\nbody\n")
if got, _ := ix.Landings("", 0); len(got) != 0 {
t.Fatalf("an unknown header line was indexed anyway: %+v", got)
}
// Same for a missing required field.
foldForumBody(t, ix, "bbbb2222", "", "odd", "2026-01-01T10:00:00",
"zit-landing: 1\nbranch: feat/x\n\nbody\n")
if got, _ := ix.Landings("", 0); len(got) != 0 {
t.Fatalf("a record with no target was indexed: %+v", got)
}
}
// An unknown state must never settle a record — a reply naming one is a
// comment with an odd first line.
func TestUnknownLandingStateDoesNotSettle(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "land it", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready"))
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
"zit-landing-state: merged\n\nwhatever\n")
if l := onlyLanding(t, ix, ""); l.State != "open" {
t.Fatalf("an unknown state settled the record as %q", l.State)
}
}
func TestLandingsFilterAndLookup(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "one", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready"))
foldForumBody(t, ix, "bbbb2222", "", "two", "2026-01-02T10:00:00",
landingBody("feat/y", "trunk", "ready"))
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
ci(t, ix, "g1", "feat/y", "2026-01-02T11:00:00", "work", "t1")
ci(t, ix, "m2", "trunk", "2026-01-03T09:59:00", "land feat/y", "t1", "g1")
foldForumBody(t, ix, "cccc3333", "bbbb2222", "", "2026-01-03T10:00:00",
landingState("accepted", "landed"))
open, err := ix.Landings("open", 0)
if err != nil {
t.Fatal(err)
}
if len(open) != 1 || open[0].Branch != "feat/x" {
t.Fatalf("open landings = %+v, want just feat/x", open)
}
all, err := ix.Landings("", 0)
if err != nil {
t.Fatal(err)
}
if len(all) != 2 {
t.Fatalf("all landings = %d, want 2", len(all))
}
// Newest activity first.
if all[0].ID != "bbbb2222" {
t.Fatalf("ordering put %q first, want bbbb2222 (newest updated)", all[0].ID)
}
l, ok, err := ix.Landing("bbbb2222")
if err != nil || !ok || l.State != "accepted" {
t.Fatalf("Landing(bbbb2222) = %+v ok=%v err=%v", l, ok, err)
}
if _, ok, err := ix.Landing("nope"); ok || err != nil {
t.Fatalf("Landing(absent) = ok=%v err=%v, want false nil", ok, err)
}
}
// An index whose forum_post rows predate this table must not show an empty
// landing list until someone reindexes — the same guarantee backfillTickets
// makes, and for the same reason.
func TestBackfillLandingsPopulatesAnUpgradedIndex(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "land it", "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-02T09:59:00", "land feat/x", "t1", "f1")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("accepted", "landed"))
// Simulate the upgrade: the posts are indexed, the table is not.
if _, err := ix.db.Exec(`DELETE FROM landing`); err != nil {
t.Fatal(err)
}
if got, _ := ix.Landings("", 0); len(got) != 0 {
t.Fatal("setup failed: landing table not empty")
}
if err := ix.backfillLandings(); err != nil {
t.Fatal(err)
}
l := onlyLanding(t, ix, "")
if l.Branch != "feat/x" || l.State != "accepted" {
t.Fatalf("backfilled %+v, want feat/x accepted", l)
}
// A populated table is left alone rather than recomputed at every open.
if _, err := ix.db.Exec(`UPDATE landing SET branch = 'sentinel'`); err != nil {
t.Fatal(err)
}
if err := ix.backfillLandings(); err != nil {
t.Fatal(err)
}
if l := onlyLanding(t, ix, ""); l.Branch != "sentinel" {
t.Fatal("backfill overwrote a populated table")
}
}
// A thread that stops being a landing must not leave a record behind.
func TestLandingRowIsDroppedWhenTheRootIsNotALanding(t *testing.T) {
ix := openIndex(t)
foldForumBody(t, ix, "aaaa1111", "", "land it", "2026-01-01T10:00:00",
landingBody("feat/x", "trunk", "ready"))
if got, _ := ix.Landings("", 0); len(got) != 1 {
t.Fatal("setup failed")
}
// Refolding the same hash with an ordinary body (a corrected mirror, a
// shunned-and-replaced record) must retract the row.
foldForumBody(t, ix, "aaaa1111", "", "land it", "2026-01-01T10:00:00", "never mind\n")
if got, _ := ix.Landings("", 0); len(got) != 0 {
t.Fatalf("a retracted record survived: %+v", got)
}
}
// A PARTIALLY populated table is healed, not left alone.
//
// The old guard was `landings > 0`, which reads "has any row" as "is
// complete". Once one landing existed the recovery never ran again, so
// a thread the one-shot did not see was never seen — and nothing said
// so, because the list answered successfully with the rows it did have.
//
// On this forge two open landings sat invisible for nineteen and nine
// days: state=open answered 0 while the DAG held both, and the rows
// only appeared when somebody wrote to the threads and the ordinary
// fold picked them up. The operator's report was "I don't see those
// landings to reject" (bbbc0a4da2e5).
func TestBackfillLandingsHealsAPartiallyPopulatedTable(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
// Two landing threads, both fully indexed as posts.
foldForumBody(t, ix, "aaaa1111", "", "land one", "2026-01-01T10:00:00",
landingBody("feat/one", "trunk", "ready"))
foldForumBody(t, ix, "cccc3333", "", "land two", "2026-01-02T10:00:00",
landingBody("feat/two", "trunk", "ready"))
if got, _ := ix.Landings("", 0); len(got) != 2 {
t.Fatalf("setup: %d landings, want 2", len(got))
}
// The state the bug leaves behind: one row present, one absent, and
// a table that is not empty so the old guard declined to look.
if _, err := ix.db.Exec(`DELETE FROM landing WHERE id = 'cccc3333'`); err != nil {
t.Fatal(err)
}
if got, _ := ix.Landings("", 0); len(got) != 1 {
t.Fatalf("setup: %d landings, want 1", len(got))
}
if err := ix.backfillLandings(); err != nil {
t.Fatal(err)
}
got, err := ix.Landings("", 0)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 {
t.Fatalf("FAIL: %d landings after backfill, want 2 — a missing row stayed missing "+
"because the table was not empty", len(got))
}
var healed bool
for _, l := range got {
if l.ID == "cccc3333" && l.Branch == "feat/two" {
healed = true
}
}
if !healed {
t.Fatalf("FAIL: the absent landing was not restored: %+v", got)
}
t.Log("ok: a table with a hole in it is filled, not declared complete")
}
// And the healing must not disturb rows that are already there, which
// is what the empty-table guard was really protecting.
func TestBackfillLandingsLeavesExistingRowsUntouched(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
foldForumBody(t, ix, "aaaa1111", "", "land one", "2026-01-01T10:00:00",
landingBody("feat/one", "trunk", "ready"))
foldForumBody(t, ix, "cccc3333", "", "land two", "2026-01-02T10:00:00",
landingBody("feat/two", "trunk", "ready"))
if _, err := ix.db.Exec(`DELETE FROM landing WHERE id = 'cccc3333'`); err != nil {
t.Fatal(err)
}
// A sentinel the fold would overwrite if the backfill recomputed
// everything rather than only what is absent.
if _, err := ix.db.Exec(`UPDATE landing SET branch = 'sentinel' WHERE id = 'aaaa1111'`); err != nil {
t.Fatal(err)
}
if err := ix.backfillLandings(); err != nil {
t.Fatal(err)
}
for _, l := range mustLandings(t, ix) {
if l.ID == "aaaa1111" && l.Branch != "sentinel" {
t.Fatalf("FAIL: an existing row was recomputed (branch %q) — healing a hole must not "+
"become a rebuild", l.Branch)
}
}
t.Log("ok: present rows are left exactly as they were")
}
func mustLandings(t *testing.T, ix *Index) []LandingSummary {
t.Helper()
got, err := ix.Landings("", 0)
if err != nil {
t.Fatal(err)
}
return got
}