Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go
T
jschoubben 93f655dac9 mesh-delivery-gate: a merge learnt from the open list seconds after it landed moves its trunk and source.main; delivery_status shows source.main
mesh-delivery#51 merged 21:43:11 and the gate read the open list at 21:43:14, before the forge's merged event: taken as
learnt late, it moved no trunk and its group's finish left it out of source.main, so the checker kept judging every
mesh-delivery head on 73be3df. A late merge within replayLate of its landing is now taken as heard as it happened, and
one a gate before this left is written at the catch-up of the start (never backwards).
2026-10-25 23:01:37 +01:00

771 lines
33 KiB
Go

package main
import (
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
)
const time30s = 31 * time.Second
// Issue 1275 (the 2026-10-19 18:05 group): a group is merged whole or not at all, in its order, and a group that is held or
// half merged is said loudly. Issue 1276/AC1: a refusal naming the member's own repository check as missing asks that check.
const (
gate, repoCheck = "mesh/merge-gate", "mesh/repo-check"
)
// statusOf sets a member head's two merge checks on the forge, and makes both required.
func setChecks(w *world, repository, sha, gateState, repoState string) {
w.forge.required = []string{gate, repoCheck}
for ctx, state := range map[string]string{gate: gateState, repoCheck: repoState} {
key := repository + "@" + sha + " " + ctx
if state == "" {
delete(w.forge.statuses, key) // no status at all: the forge says it is missing
continue
}
w.forge.statuses[key] = state + " said → x"
}
}
func allGreen(w *world) {
setChecks(w, gCat, gHk, "success", "success")
setChecks(w, gCtl, gHc, "success", "success")
}
func refusedWords(repository string) error {
return errors.New("the forge refused gitea_merge_pull_request: " + repository + " is not merged: main requires " +
"mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing). A pull request merges once its checks succeed")
}
func urgentLines(w *world) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.Severity == "urgent" {
out = append(out, l)
}
}
return out
}
// 1275/AC2: an approved group whose member has a required status that is not a success at its head when the merge starts
// is merged not at all, though the first member in order is fine; the group says which member and which status.
func TestAnApprovedGroupWithAMemberWhoseRequiredStatusIsNotASuccessMergesNothing(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing after the ask
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged with a required status missing: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || !strings.Contains(lines[0].Says, "feat/g") || !strings.Contains(lines[0].Says, IDOf(gCtl, gHc)) ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("the held group is not said as one urgent line naming group, member, status and need: %+v", lines)
}
// The status turns success: the whole group merges.
setChecks(w, gCtl, gHc, "success", "success")
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.later(time30s)
w.settleAll()
if len(w.forge.merges) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("merges %v, lines %+v; wanted both merged and no line", w.forge.merges, urgentLines(w))
}
}
// 1275/AC4: a group half merged (the second member refused after the first merged) stays raised, pass after pass, until
// the second is merged; then it goes.
func TestAHalfMergedGroupStaysRaisedUntilEveryMemberIsMerged(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCtl: refusedWords("novox/mesh-controller#1")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
for range 3 {
w.later(time30s)
w.settleAll()
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCtl, gHc) || !strings.Contains(lines[0].Says, "feat/g") {
t.Fatalf("the half merged group is not raised: %+v", lines)
}
}
// A half merged group with no refusal on record (the pass died between members) is raised too.
d := w.delivery(IDOf(gCtl, gHc))
d.Ready.RefusedSince = nil
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "half merged") {
t.Fatalf("a half merged group without a refusal is not raised: %+v", lines)
}
w.forge.refuseRepo = nil
setChecks(w, gCtl, gHc, "success", "success")
w.h.PullMerged(merged(gCtl, 1, gHc, gT1))
w.settleAll()
if lines := urgentLines(w); len(lines) != 0 {
t.Fatalf("the group is whole and still raised: %+v", lines)
}
}
// 1275/AC5: a member's required status turning not-success while the ask waits withdraws the ask at the router; the
// answer to the withdrawn ask merges nothing; the group is asked again once every member may merge.
func TestAStatusThatTurnsWhileTheAskWaitsWithdrawsItAndItsAnswerMergesNothing(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
ask := w.op.open()[0]
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "pending"})
setChecks(w, gCat, gHk, "success", "pending")
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != ask {
t.Fatalf("the ask was not withdrawn at the router: %v", w.op.cancelled)
}
w.op.answer(ask, OptionApprove) // an answer that arrives late
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("the answer to a withdrawn ask merged: %v", w.forge.merges)
}
if w.op.count() != 1 {
t.Fatalf("asked again while a status was still pending: %d", w.op.count())
}
setChecks(w, gCat, gHk, "success", "success")
w.later(time30s)
w.settleAll()
if w.op.count() != 2 {
t.Fatalf("not asked again once every member may merge: %d", w.op.count())
}
}
// 1275/AC3 (review round 1): a member that is both refused and held for a required status is said by one stalled line,
// not two with the same id.
func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
setChecks(w, gCat, gHk, "success", "") // now its status is missing too; the next pass is held
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "success"})
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if g := w.h.groups["feat/g"]; g.Hold == nil {
t.Fatalf("the group was not held for the missing status")
}
n := 0
for _, l := range w.h.Stalled() {
if l.ID == IDOf(gCat, gHk) {
n++
}
}
if n != 1 {
t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled())
}
}
// 1275/AC6, as novox/hq ADR 0505 §4 changed it: a member refused because its verdict is out of date (the trunk moved; its
// fresh check runs) is no stalled line while the gate answers it, and urgent once ten minutes passed with the group not
// merged; the pass stops at the refusal all the same.
func TestAnOutOfDateRefusalIsNoLineWhileAnsweredAndUrgentAfterTenMinutes(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the pass did not stop at the refusal: %v", w.forge.merges)
}
sev := func() []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
if got := sev(); len(got) != 0 {
t.Fatalf("an out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(9 * time.Minute)
if got := sev(); len(got) != 0 {
t.Fatalf("at nine minutes it is %v; wanted still no line", got)
}
w.later(2 * time.Minute)
if got := sev(); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6: every other refusal (here a missing status) is urgent at once.
func TestAMissingStatusRefusalIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if lines := w.h.Stalled(); len(lines) != 1 || lines[0].Severity != "urgent" {
t.Fatalf("a missing status refusal is not urgent at once: %+v", lines)
}
}
func severities(w *world) []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
// 1275/AC6 (b), as ADR 0505 §4 changed it: the same out of date refusal on a LATER member, after the earlier one merged,
// is no line at once and urgent after ten minutes: the half merged group does not say it at once.
func TestAnOutOfDateRefusalOnALaterMemberIsNoLineThenUrgent(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
w.settleAll()
if got := severities(w); len(got) != 0 {
t.Fatalf("a later member's out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(11 * time.Minute)
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6 (c): words naming an out of date check and also a missing status are urgent at once.
func TestARefusalNamingOutOfDateAndAMissingStatusIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: novox/mesh-catalog#2 is not merged: its verdict was " +
"judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. The check of head cccc merged into trunk bbbb was " +
"asked. main requires mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing)")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("a refusal with two causes is %v; wanted urgent at once", got)
}
}
// 1275/AC6 (d): words mesh-delivery cannot classify are urgent at once.
func TestARefusalInUnknownWordsIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: something nobody foresaw")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("an unclassified refusal is %v; wanted urgent at once", got)
}
}
// The merge tool's refusals, copied from gitea merge.ts with their line, `${...}` filled in.
var mergeToolRefusals = []struct {
line string
words string
outOfDated bool
}{
{"merge.ts:121", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"Its check is already asked and not yet answered (building), so it is not asked again; took 3s. Ask again at cccccccccccc once " +
"both statuses pass: this tool never merges by itself (novox/hq ADR 0323). A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:147", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"The check of head cccccccccccc merged into trunk bbbbbbbbbbbb was asked; took 3s. Ask again at cccccccccccc once both " +
"statuses pass: this tool never merges by itself (novox/hq ADR 0323). In a busy repository another merge may move the trunk " +
"again first; this is then refused and asked once more. A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:144", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb, " +
"and its check could not be asked: the forge is away. Nothing was merged", false},
{"merge.ts:156", "novox/app#7 is not merged: main requires mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check " +
"(pending). A pull request merges once its checks succeed", false},
{"merge.ts:165", "novox/app#7 is not merged: its verdict read siblings, and which ones is not held here, so the group cannot " +
"be weighed. Ask its check again at cccccccccccc. Nothing was merged", false},
{"merge.ts:172", "novox/app#7 is not merged: its verdict was judged beside 1 sibling(s), and a sibling is not ready. A member " +
"judged beside its siblings merges once its group passed at the heads that verdict read (novox/hq ADR 0382, 0421). " +
"Nothing was merged", false},
}
// 1275/AC6 (e), as ADR 0505 §4 changed it: only the merge tool's two out of date refusals (:121, :147) are answered by
// the gate and said as no line at once; the one saying its check could not be asked (:144) and every other is urgent at
// once.
func TestOnlyTheMergeToolsTwoOutOfDateRefusalsWaitWhileAnswered(t *testing.T) {
for _, c := range mergeToolRefusals {
if got := outOfDateRefusal(c.words); got != c.outOfDated {
t.Errorf("%s: outOfDateRefusal = %v, wanted %v", c.line, got, c.outOfDated)
}
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: " + c.words)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
got := severities(w)
if c.outOfDated && len(got) != 0 {
t.Errorf("%s: said %v at once, wanted no line while it is answered", c.line, got)
}
if !c.outOfDated && (len(got) != 1 || got[0] != "urgent") {
t.Errorf("%s: severity %v at once, wanted urgent", c.line, got)
}
}
}
// 1275/AC6 (f): the severity never steps down: a missing status refusal, then an out of date one, stays urgent.
func TestASeverityOnceUrgentNeverStepsDownToAWarning(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.forge.refuseRepo = map[string]error{gCat: errAskedAgain}
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if d := w.delivery(IDOf(gCat, gHk)); !outOfDateRefusal(d.Ready.Answer) {
t.Fatalf("the second refusal was not the out of date one: %q", d.Ready.Answer)
}
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("severity %v after an out of date refusal that followed an urgent one; wanted urgent", got)
}
}
// fakeConditions is the gate's bucket conditions and the operator-channel seat's subject for conditions: what it keeps,
// and every condition said, in order.
type fakeConditions struct {
mu sync.Mutex
kept map[string]string
told []asks.Condition
}
func (f *fakeConditions) Said() (map[string]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := map[string]string{}
for k, v := range f.kept {
out[k] = v
}
return out, nil
}
func (f *fakeConditions) PutSaid(said map[string]string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.kept = map[string]string{}
for k, v := range said {
f.kept[k] = v
}
return nil
}
func (f *fakeConditions) say(c asks.Condition) error {
if _, err := asks.EncodeCondition(c); err != nil {
return err // what the router would refuse
}
f.mu.Lock()
defer f.mu.Unlock()
f.told = append(f.told, c)
return nil
}
// last is the newest condition said for a delivery id, or nil.
func (f *fakeConditions) last(id string) *asks.Condition {
f.mu.Lock()
defer f.mu.Unlock()
for i := len(f.told) - 1; i >= 0; i-- {
if f.told[i].Subject.ID == id {
c := f.told[i]
return &c
}
}
return nil
}
// hearConditions has the gate say its conditions into a fake seat and bucket.
func hearConditions(w *world) *fakeConditions {
f := &fakeConditions{kept: map[string]string{}}
w.h.Say, w.h.Conditions = f.say, f
return f
}
// handedInOrder answers each member the gate hands the forge bridge as the forge does on 2026-10-25: the forge's word that
// it merged (gitea.pull.merged) first, the bridge's word after; or the bridge's first, as forgeFirst says. It returns the
// repositories in the order they were handed.
func handedInOrder(w *world, forgeFirst bool) []string {
var handed []string
numbers := map[string]int{}
for i := 0; i < 4; i++ {
var next *sdk.MergePull
for _, p := range w.merges.last("feat/g").Pulls {
if p.State == sdk.MergeMerging {
p := p
next = &p
}
}
if next == nil || numbers[next.Repository] > 0 {
break
}
handed = append(handed, next.Repository)
numbers[next.Repository] = next.Number
mergeCommit := strings.Repeat(string(rune('a'+i)), 40)
forge := func() { w.h.PullMerged(merged(next.Repository, next.Number, next.Head.Head, mergeCommit)) }
bridge := func() { bridgeMerged(w, next.Repository, next.Number, next.Head.Head, mergeCommit) }
if forgeFirst {
forge()
w.settleAll() // ten seconds pass between the two words on 2026-10-25: the gate looks in between
bridge()
} else {
bridge()
w.settleAll()
forge()
}
w.settleAll()
}
return handed
}
// Issue 1275, met again 2026-10-25 (group fix/puller-wakes-on-capacity): every member of an approved group is handed to
// the forge bridge, in the group's order, the next on the event that the one before merged; a group whose pass stops is
// said to the operator as a condition, which is cleared once it is whole; and every word a group's state is said in is
// one the merge decides on.
func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
const (
sdkRepo, hS = "novox/mesh-sdk", "a8fbabbfc37369f9d1f6977c72e5523aff3d494c"
catRepo, hK = "novox/mesh-catalog", "23c827e7910c233608af4e4a8056cbc362dcb755"
ctlRepo, hC = "novox/mesh-controller", "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0"
)
sdkMember := member{sdkRepo, 80, hS, "go/stdio.go,go/buildstep/pull.go"}
catMember := member{catRepo, 479, hK, "modules/mesh-build-go/asks.go,modules/mesh-build-go/go.mod,modules/mesh-build-go/go.sum"}
ctlMember := member{ctlRepo, 7, hC, "cmd/mesh-controller/main.go"}
inOrder := func(members []member, forgeFirst bool, want ...string) func(t *testing.T) {
return func(t *testing.T) {
w := approvedGroupOf(t, members...)
w.merges.manual = true
allGreenFor(w, members...)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := handedInOrder(w, forgeFirst); strings.Join(got, " ") != strings.Join(want, " ") {
t.Fatalf("handed %v, wanted %v, every member in the group's order", got, want)
}
if g := w.h.groups["feat/g"]; g == nil {
t.Fatal("the group is gone")
} else if state, why := GroupState(g, w.h.membersOf(g)); state != GroupMerged {
t.Fatalf("the group is %s (%s), not merged whole", state, why)
}
if w.source.main == nil || len(w.source.main.Heads) != len(want) {
t.Fatalf("source.main does not hold the group as one revision: %+v", w.source.main)
}
}
}
cases := []struct {
name string
run func(t *testing.T)
}{
// 1275/AC3 (2026-10-25): the case of the day. The catalogue member pins the sdk member's commit; the forge's word
// of a merge arrives before the bridge's. Before the fix the catalogue member went first, and the sdk member was
// never handed: its group read merging, a word the turn to merge did not know.
{"sdk first, then the member that pins it, the forge's word first", inOrder([]member{catMember, sdkMember}, true, sdkRepo, catRepo)},
{"sdk first, then the member that pins it, the bridge's word first", inOrder([]member{sdkMember, catMember}, false, sdkRepo, catRepo)},
{"a member that pins nothing before one that changes a go.mod", inOrder([]member{catMember, ctlMember}, true, ctlRepo, catRepo)},
{"three members", inOrder([]member{ctlMember, catMember, sdkMember}, true, sdkRepo, ctlRepo, catRepo)},
// 1275/AC3: the merge tool refuses the first member in order; the second is not merged, and the group is said
// urgent, naming the group, the refused member and the forge's words, as a stalled line and to the operator.
{"a refused first member stops the group and is said urgent", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || !strings.Contains(w.forge.merges[0], "mesh-catalog#2") {
t.Fatalf("wanted only the first member asked: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCat, gHk) || !strings.Contains(lines[0].Says, "feat/g") ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("not said as one urgent line: %+v", lines)
}
if c := told.last(IDOf(gCat, gHk)); c == nil || c.State != asks.StateRaised || c.Severity != asks.SeverityUrgent ||
!strings.Contains(c.Summary, "feat/g") {
t.Fatalf("the operator was not told, urgent: %+v", c)
}
}},
// 1275/AC4: a group half merged (its pass stopped after the first member) raises one urgent condition, said once
// however many looks follow, and cleared once the group is whole.
{"a half-merged group is raised once and cleared once whole", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.merges.manual = true
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
bridgeMerged(w, gCat, 2, gHk, gT1)
setChecks(w, gCtl, gHc, "success", "") // the second member's status went missing: the pass stops at it
w.settleAll()
for range 3 {
w.later(time30s)
w.settleAll()
}
id := IDOf(gCtl, gHc)
raised := 0
for _, c := range told.told {
if c.Subject.ID == id && c.State == asks.StateRaised {
raised++
}
}
if c := told.last(id); raised != 1 || c == nil || c.Severity != asks.SeverityUrgent || !strings.Contains(c.Summary, "feat/g") ||
!strings.Contains(c.Summary, "half merged (merged 1 of 2)") {
t.Fatalf("raised %d times, last %+v; wanted once, urgent, naming the group half merged", raised, c)
}
setChecks(w, gCtl, gHc, "success", "success")
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.settleAll() // the status is an event: the pass goes on with no timer
handed := false
for _, p := range w.merges.last("feat/g").Pulls {
handed = handed || (p.Repository == gCtl && p.State == sdk.MergeMerging)
}
if !handed {
t.Fatalf("the status turned success and the second member was not handed: %+v", w.merges.last("feat/g"))
}
w.h.PullMerged(merged(gCtl, 1, gHc, gT1))
bridgeMerged(w, gCtl, 1, gHc, gT1)
w.settleAll()
if c := told.last(id); c == nil || c.State != asks.StateCleared {
t.Fatalf("the group is whole and its condition is not cleared: %+v", c)
}
}},
// A condition the gate raised before it restarted, whose delivery stalls no more, is cleared at the first look.
{"a condition kept from before a restart is cleared", func(t *testing.T) {
w := newWorld(t)
told := hearConditions(w)
told.kept[conditionKeyOf("novox/app@0123456789ab")] = "urgent||"
w.settleAll()
if c := told.last("novox/app@0123456789ab"); c != nil {
t.Fatalf("cleared before the forge's open pull requests were read: %+v", c)
}
w.h.wantForge()
w.settleAll()
if c := told.last("novox/app@0123456789ab"); c == nil || c.State != asks.StateCleared || len(told.kept) != 0 {
t.Fatalf("not cleared: %+v, kept %v", c, told.kept)
}
}},
// Review round 1, blocker 1: a pull request closed or merged on the forge with no event heard (de-spiegel #4,
// closed 2026-10-17, ready here eight days later) settles at the next complete read of the open list, and no
// condition is raised for it; a merge learnt so does not move source.main.
{"a pull request gone from the open list settles and raises nothing", func(t *testing.T) {
for _, merged := range []bool{false, true} {
w := newWorld(t)
w.op.auto = false
const repo, head = "novox/de-spiegel", "fdcbaefb3123fdcbaefb3123fdcbaefb3123fdcb"
w.h.PullUpdated(pr(repo, 4, head, "feat/x", "src/a.go"))
w.h.Checked(judgedAt(repo, 4, head, "pass", "t1t1t1t1t1t1"))
w.settleAll()
w.forge.refuse = errors.New("novox/de-spiegel#4 merges into main, which no protection rule requiring the checks guards")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
id := IDOf(repo, head)
if len(w.h.Stalled()) == 0 {
t.Fatalf("the refused head is not stalled to begin with")
}
p := ForgePull{Number: 4, State: "closed", HeadSHA: head, Base: "main"}
if merged {
p.Merged, p.MergeCommit = true, strings.Repeat("e", 40)
}
w.forge.pulls[repo+"#4"] = p
w.forge.open = []OpenPull{{Owner: "novox", Repo: "other", Number: 1, Base: "main", HeadSHA: "abab"}}
told := hearConditions(w)
moves := len(w.source.moves)
w.h.wantForge()
w.settleAll()
want := Closed
if merged {
want = Merged
}
if st := w.state(id); st != want {
t.Fatalf("merged %v: the delivery is %s, wanted %s", merged, st, want)
}
if c := told.last(id); c != nil {
t.Fatalf("merged %v: a condition was said for it: %+v", merged, c)
}
if len(w.source.moves) != moves {
t.Fatalf("merged %v: source.main moved for a merge learnt by its absence: %+v", merged, w.source.moves)
}
}
}},
// Review round 2: a merge learnt after the fact moves no other delivery's trunk (an approved group of the same
// repository keeps its trunk, its state and its composed check), takes the forge's time, and a catch-up after it
// does not move source.main.
{"a merge learnt late moves no trunk and no source", func(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.merges.manual = true
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
const lateHead, lateMerge = "9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a", "8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b"
w.h.PullUpdated(pr(gCat, 9, lateHead, "fix/old", "modules/app/x.go"))
w.h.Checked(judged(gCat, 9, lateHead, "pass", gTrunk0))
w.settleAll()
late := IDOf(gCat, lateHead)
trunkOf := func() map[string]string {
out := map[string]string{}
for _, id := range []string{IDOf(gCat, gHk), IDOf(gCtl, gHc)} {
out[id] = w.delivery(id).TrunkNow + " " + string(w.delivery(id).State)
}
return out
}
before, moves := trunkOf(), len(w.source.moves)
w.forge.pulls[gCat+"#9"] = ForgePull{Number: 9, State: "closed", Merged: true, MergeCommit: lateMerge,
MergedAt: "2026-10-01T09:00:00Z", HeadSHA: lateHead, Base: "main"}
w.forge.open = []OpenPull{{Owner: "novox", Repo: "mesh-catalog", Number: 2, Base: "main", HeadSHA: gHk},
{Owner: "novox", Repo: "mesh-controller", Number: 1, Base: "main", HeadSHA: gHc}}
w.h.wantForge()
w.settleAll()
d := w.delivery(late)
if d.State != Merged || !d.MergedAt.Equal(time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)) || !d.LearntLate {
t.Fatalf("the late merge is %s at %v (learnt late %v)", d.State, d.MergedAt, d.LearntLate)
}
if after := trunkOf(); fmt.Sprint(after) != fmt.Sprint(before) {
t.Fatalf("the group's trunk or state moved: %v, was %v", after, before)
}
if g := w.h.groups["feat/g"]; g.Check == nil || g.Check.OutOfDate != "" {
t.Fatalf("the group's composed check went out of date: %+v", g.Check)
}
w.h.SourceCatchUp()
w.settleAll()
if len(w.source.moves) != moves {
t.Fatalf("source.main moved for a merge learnt late: %+v", w.source.moves[moves:])
}
}},
// mesh-delivery#51 (2026-10-25 21:43): the open list was read seconds after the bridge merged it and before the forge's
// merged event, so it was learnt late, moved no trunk, and its group's finish left it out of source.main. A merge
// learnt within replayLate of its landing is taken as heard as it happened; one a gate before the fix took late is
// written at the catch-up of the start.
{"a merge learnt from the open list seconds after it landed moves source.main", func(t *testing.T) {
const repo, head, merge = "novox/mesh-delivery", "4628ba26b41a4628ba26b41a4628ba26b41a4628", "db53ec975f01db53ec975f01db53ec975f01db53"
for _, oldGate := range []bool{false, true} {
w := newWorld(t)
w.merges.manual = true
w.h.PullUpdated(pr(repo, 51, head, "fix/1945", "modules/a/a.go"))
w.h.Checked(judgedAt(repo, 51, head, "pass", "t1t1t1t1t1t1"))
w.settleAll()
w.source.main = &sdk.Source{V: sdk.Version, Group: "before", Heads: map[string]string{repo: "73be3df28cd4"}}
w.forge.pulls[repo+"#51"] = ForgePull{Number: 51, State: "closed", Merged: true, MergeCommit: merge,
MergedAt: w.h.Now().Add(-3 * time.Second).UTC().Format(time.RFC3339), HeadSHA: head, Base: "main"}
w.forge.open = []OpenPull{{Owner: "novox", Repo: "other", Number: 1, Base: "main", HeadSHA: "abab"}}
w.h.wantForge()
w.settleAll()
if oldGate { // what the gate before the fix left
w.h.mu.Lock()
kept := w.h.deliveries[IDOf(repo, head)]
kept.LearntLate, kept.InSource, kept.Ready.SourceMoved = true, true, true
w.h.mu.Unlock()
w.source.main.Heads[repo] = "73be3df28cd4"
w.h.SourceCatchUp()
w.settleAll()
}
d := w.delivery(IDOf(repo, head))
if d.State != Merged || (!oldGate && d.LearntLate) || w.source.main.Heads[repo] != merge {
t.Fatalf("old gate %v: %s, learnt late %v, source.main %v", oldGate, d.State, d.LearntLate, w.source.main.Heads)
}
if heads, _ := w.h.SourceMain()["heads"].(map[string]string); heads[repo] != merge {
t.Fatalf("delivery_status's source-main says %v", w.h.SourceMain())
}
}
}},
// Review round 1, blocker 2 (1275/AC6): an out-of-date refusal at t0 with no other event is said urgent at t0+10
// minutes: the gate asks itself for a look then.
{"an out-of-date refusal turns urgent on the clock alone", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
t0 := w.h.Now()
w.h.wanted.mu.Lock()
at := w.h.wanted.at
w.h.wanted.mu.Unlock()
if at.IsZero() || at.Sub(t0) > outOfDateFor {
t.Fatalf("no look is wanted by t0+%s: %v", outOfDateFor, at)
}
for i := 0; i < 5 && w.h.Now().Before(t0.Add(outOfDateFor)); i++ { // the timer fires a look, and nothing else happens
w.h.wanted.mu.Lock()
at = w.h.wanted.at
w.h.wanted.mu.Unlock()
if at.IsZero() {
break
}
w.later(at.Sub(w.h.Now()))
w.h.React()
}
c := told.last(IDOf(gCat, gHk))
if c == nil || c.State != asks.StateRaised || c.Severity != asks.SeverityUrgent {
t.Fatalf("at t0+%s with no event: %+v", w.h.Now().Sub(t0), c)
}
}},
// Review round 1, blocker 3: a gate that restarts while a head still waits on the forge does not clear its
// condition (the head's first sight is in memory alone, so its bound runs again), and does not say it again.
{"a restart keeps the condition of a head still waiting", func(t *testing.T) {
w := newWorld(t)
told := hearConditions(w)
const head = "bfe82315f42c8f4509d0598c32569a202d5eada6"
id := IDOf("novox/hq", head)
w.forge.open = []OpenPull{{Owner: "novox", Repo: "hq", Number: 243, Base: "main", HeadSHA: head, Required: []string{"mesh/repo-check"}}}
w.h.wantForge()
w.settleAll()
w.later(UncheckedAfter + time.Minute)
w.settleAll()
if c := told.last(id); c == nil || c.State != asks.StateRaised {
t.Fatalf("the unchecked head is not raised: %+v", c)
}
before := len(told.told)
again := newWorldOver(t, w.store, w.forge, w.h.Now()) // the gate restarts
again.h.Say, again.h.Conditions = told.say, told
again.h.wantForge()
again.settleAll()
again.later(time.Minute)
again.settleAll()
if len(told.told) != before {
t.Fatalf("the restart said something of a head still waiting: %+v", told.told[before:])
}
w.forge.open = nil // it closes
again.h.wantForge()
again.settleAll()
if c := told.last(id); c == nil || c.State != asks.StateCleared {
t.Fatalf("closed, and its condition is not cleared: %+v", c)
}
}},
// The kind behind the day's fault: a decision on a group's state named a word GroupState no longer says. Every word
// it says is one groupStatusState knows, and the merge goes on exactly in ready and merging.
{"every word of a group's state is decided on", func(t *testing.T) {
for _, word := range groupWords {
if groupStatusState(word) == "pending" && word != GroupChecking {
t.Errorf("groupStatusState does not know %q", word)
}
if may := groupMayMerge(word); may != (word == GroupReady || word == GroupMerging) {
t.Errorf("groupMayMerge(%q) = %v", word, may)
}
}
}},
}
for _, c := range cases {
t.Run(c.name, c.run)
}
}
// allGreenFor makes both merge checks a success at every member's head.
func allGreenFor(w *world, ms ...member) {
for _, m := range ms {
setChecks(w, m.repo, m.head, "success", "success")
}
}
// bridgeMerged is the forge bridge's word that it merged a member.
func bridgeMerged(w *world, repository string, number int, head, merge string) {
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
Trees: map[string]string{"app": "tree"}})
}