Skip to content
quarry Artifact [69a78834de]
Login

Artifact [69a78834de]

Artifact 69a78834de3e44f9efcc99230879a85a7e79d6251f5e5ef2368d47ceb0bf8956:


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)
	}
}