package index
import (
"testing"
)
// RFC 0007: the merge is the fact and the reply is the claim. A reply
// that says "accepted" with nothing merged leaves the record OPEN and
// records the claim; the merge check-in alone, with no reply at all,
// settles it. Unconditional — no policy row is consulted — which is
// what lets a wipe-and-reingest, or a protection row added later,
// change nothing about how history renders.
func TestAcceptReplyWithoutAMergeIsAClaim(t *testing.T) {
ix := openIndex(t)
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "Ship it")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("accepted", "I say so"))
l := onlyOne(t, ix, "aaaa1111")
if l.State != "open" {
t.Fatalf("state %q after a reply-only accept, want open", l.State)
}
if l.ClaimedBy != "bbbb2222" {
t.Errorf("claimed_by %q, want the reply that made the claim", l.ClaimedBy)
}
if l.SettledBy != "" {
t.Errorf("settled_by %q on an unsettled record", l.SettledBy)
}
// On the timeline it is conversation, not a verdict.
if k, _, c := eventRow(t, ix, "bbbb2222"); k != "lc" || c != "Acceptance claimed on landing: Ship it" {
t.Errorf("the claim is filed as %q/%q", k, c)
}
}
func TestAMergeWithNoReplySettlesTheRecord(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "Ship it")
ci(t, ix, "f1", "feat-x", "2026-01-01T11:00:00", "work", "t1")
// An owner merged from a clone and pushed; nobody posted a reply.
ci(t, ix, "m1", "trunk", "2026-01-02T10:00:00", "land feat-x", "t1", "f1")
// The check-ins arrived AFTER the landing was folded, so the
// record was open until something re-read it. The refresh after
// recomputeBranches is that something.
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)
}
l := onlyOne(t, ix, "aaaa1111")
if l.State != "accepted" {
t.Fatalf("state %q with the merge in the DAG, want accepted", l.State)
}
if l.SettledBy != "" || l.ClaimedBy != "" {
t.Errorf("settled_by=%q claimed_by=%q, want both empty: nothing was posted", l.SettledBy, l.ClaimedBy)
}
// And the approver named on the timeline is whoever wrote the merge.
rows := []TimelineEntry{{Kind: "ci", Hash: "m1", IsMerge: true}}
if err := ix.attachLandings(rows); err != nil {
t.Fatal(err)
}
if rows[0].LandingApprover != "dan" {
t.Errorf("approver %q, want the merge's author", rows[0].LandingApprover)
}
}
// A claim, then the merge: the claim clears and the record settles.
// The reply that claimed becomes the settling reply, because it was
// right — just early.
func TestAClaimBecomesAVerdictWhenTheMergeArrives(t *testing.T) {
ix := openIndex(t)
ci(t, ix, "t1", "trunk", "2026-01-01T09:00:00", "base")
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "Ship it")
ci(t, ix, "f1", "feat-x", "2026-01-01T11:00:00", "work", "t1")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("accepted", "landing"))
if l := onlyOne(t, ix, "aaaa1111"); l.State != "open" || l.ClaimedBy != "bbbb2222" {
t.Fatalf("precondition: %+v", l)
}
ci(t, ix, "m1", "trunk", "2026-01-02T10:00:05", "land feat-x", "t1", "f1")
// Any later post re-folds the thread; a comment will do.
foldForumBody(t, ix, "cccc3333", "aaaa1111", "", "2026-01-02T11:00:00", "nice")
l := onlyOne(t, ix, "aaaa1111")
if l.State != "accepted" || l.SettledBy != "bbbb2222" || l.ClaimedBy != "" {
t.Fatalf("after the merge: %+v, want accepted, settled by the reply, no claim", l)
}
if k, _, c := eventRow(t, ix, "bbbb2222"); k != "l" || c != "Landing accepted: Ship it" {
t.Errorf("the reply is still filed as a claim: %q/%q", k, c)
}
}
// Rejection has no DAG fact to read, so the reply stays the record.
func TestRejectionIsStillTheReplysToSay(t *testing.T) {
ix := openIndex(t)
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "No thanks")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("rejected", "not this"))
if l := onlyOne(t, ix, "aaaa1111"); l.State != "rejected" || l.SettledBy != "bbbb2222" {
t.Fatalf("%+v, want rejected by the reply", l)
}
}
// The merge must be the landing's own: a merge of the branch from
// BEFORE the landing opened is history the proposal was made on top
// of, not the proposal landing.
func TestAMergeBeforeTheLandingOpenedDoesNotCount(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", "old work", "t1")
ci(t, ix, "m0", "trunk", "2026-01-01T09:45:00", "landed once already", "t1", "f1")
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "Round two")
if l := onlyOne(t, ix, "aaaa1111"); l.State != "open" {
t.Fatalf("state %q — an earlier merge settled a later proposal", l.State)
}
}
// The branch is already on the target — merged by hand before the
// landing was opened, or the target simply contains its tip. zit
// settles such a landing as accepted with nothing to merge; the fold
// must read the same, by ancestry, rather than call the reply a claim.
func TestAcceptOfAContainedBranchIsAcceptedNotAClaim(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")
// trunk already carries f1 (a merge from before the landing).
ci(t, ix, "t2", "trunk", "2026-01-01T09:45:00", "carried in", "t1", "f1")
landingOn(t, ix, "aaaa1111", "feat-x", "2026-01-01T10:00:00", "Already there")
foldForumBody(t, ix, "bbbb2222", "aaaa1111", "", "2026-01-02T10:00:00",
landingState("accepted", "nothing to land"))
l := onlyOne(t, ix, "aaaa1111")
if l.State != "accepted" || l.SettledBy != "bbbb2222" || l.ClaimedBy != "" {
t.Fatalf("contained branch: %+v, want accepted by the reply, no claim", l)
}
// A reply-only accept with the branch NOT contained is still a claim.
ci(t, ix, "g1", "feat-y", "2026-01-01T09:30:00", "other work", "t1")
landingOn(t, ix, "cccc3333", "feat-y", "2026-01-01T10:00:00", "Not there")
foldForumBody(t, ix, "dddd4444", "cccc3333", "", "2026-01-02T10:00:00",
landingState("accepted", "I say so"))
if l := onlyOne(t, ix, "cccc3333"); l.State != "open" || l.ClaimedBy != "dddd4444" {
t.Fatalf("uncontained reply-only accept: %+v, want a claim", l)
}
}