mesh/delivery-group group feat/341-a-merge-check-judges-the-merge checking: 0 of 3 member(s) ready
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
A merge check's verdict now names the trunk commit it judged (novox/hq ADR 0323, issue 341), but the forge kept it green after another merge moved the trunk, the merge tool merged on it, and mesh-delivery kept the delivery ready. The forge's holder puts both statuses of every open pull request into a moved trunk back to pending with the reason and runs nothing; the merge tool compares the verdict's trunk commit with the trunk's head, asks the check once and never merges by itself; mesh-delivery sends a ready delivery back to proposed, counts only a verdict for the trunk's head, and composes a group again for a rebased member instead of holding it.
258 lines
11 KiB
Go
258 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The table walked (novox/hq ADR 0239 decision 2): every row it holds can be taken when its guard holds and
|
|
// is refused when it does not; every pair it does not hold is refused, by name; and the rules the table
|
|
// exists for are true of the table itself, not of the code around it.
|
|
|
|
// aDeliveryFor is a delivery whose facts make the row's guard hold.
|
|
func aDeliveryFor(r Row) (*Delivery, Facts) {
|
|
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
|
d := &Delivery{ID: "novox/app@c0ffee000000", Repository: "novox/app", Commit: "c0ffee000000", Number: 7}
|
|
f := Facts{Now: now}
|
|
switch r.Event {
|
|
case EvAppeared:
|
|
d.MergedAs, d.HeldWhy = "c0ffee000000", "no pull request"
|
|
case EvAdopted, EvGo:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkRolling}
|
|
if r.From[0] == Held {
|
|
d.Walk.WaitedFor, d.Walk.GoBy = "mesh-delivery", "a person (jochen)"
|
|
}
|
|
case EvNewHead:
|
|
d.NewerHead = "beef"
|
|
case EvClosed:
|
|
d.ClosedUnmerged = true
|
|
case EvMerged:
|
|
d.MergedAs, d.Walk = "aaaa1111", &WalkSeen{ID: "plan-1", State: walkBuilding, Waits: true, WaitedFor: "mesh-delivery"}
|
|
case EvMergedUnchecked:
|
|
d.MergedAs = "aaaa1111"
|
|
case EvHistory, EvRetire:
|
|
d.MergedAs, d.FromForge = "aaaa1111", true
|
|
d.MergedAt, d.Created = now.Add(-48*time.Hour), now
|
|
if r.Event == EvHistory {
|
|
d.History = "merged two days before this owner heard of it"
|
|
} else {
|
|
f.Why, f.By = "history adopted at the switch", "jochen"
|
|
}
|
|
case EvChecked, EvAccepted:
|
|
d.Check = &Verdict{Gate: "pass"}
|
|
case EvRefused:
|
|
d.Check = &Verdict{Gate: "fail"}
|
|
case EvRecheck, EvRelease, EvStop:
|
|
f.Why, f.By = "a person's reason", "jochen"
|
|
if r.Event == EvStop && !r.Act {
|
|
f = Facts{Now: now}
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkFailed, StoppedBy: "mesh-delivery for jochen"}
|
|
if r.From[0] == Published {
|
|
// Its walk ended done before its word, and nothing delivered it (novox/hq issue 474).
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone, Waits: true, WaitedFor: "mesh-delivery", MovedSaid: true}
|
|
d.Stranded = "its walk plan-1 ended done before its word without building app"
|
|
}
|
|
}
|
|
case EvReask:
|
|
f.Why, f.By = "delivery stalled", byH2
|
|
d.CheckAsk = &CheckAsk{ID: "check-1", At: now.Add(-2 * time.Hour), By: byOwner}
|
|
case EvHold:
|
|
d.HeldWhy, d.MergedAs = "its group's check did not pass", "aaaa1111"
|
|
case EvDone:
|
|
switch {
|
|
case r.From[0] == Published:
|
|
d.Plan = &DeliveryPlan{Summary: "moves nothing"}
|
|
case r.From[0] == Proposed:
|
|
d.MergedAs, d.Plan = "aaaa1111", &DeliveryPlan{Summary: "builds nothing"}
|
|
default:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone}
|
|
}
|
|
case EvCovered:
|
|
d.MergedAs, d.CoveredBy = "aaaa1111", "delivered with novox/app@bbbb (merged as bbbb2222, walk plan-2): app"
|
|
if r.From[0] == Published {
|
|
// Its walk ended done before its word; a later merge delivered its modules (novox/hq issue 474).
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone, Waits: true, WaitedFor: "mesh-delivery", MovedSaid: true}
|
|
}
|
|
if r.From[0] == Delivering {
|
|
// Answered by its successor in one walk (ADR 0276): the walk done, carrying a later merge.
|
|
d.CoveredBy, d.AnsweredBy, d.Carrier = "", "bbbb2222", "novox/app@bbbb (merged as bbbb2222, walk plan-2)"
|
|
d.Walk = &WalkSeen{ID: "plan-2", State: walkDone}
|
|
}
|
|
case EvRewalk:
|
|
d.MergedAs, d.AnsweredBy = "aaaa1111", "bbbb2222"
|
|
d.Walk = &WalkSeen{ID: "plan-2", State: walkFailed}
|
|
case EvTrunkMoved:
|
|
d.Check = &Verdict{Gate: "pass", Head: "c0ffee000000", Trunk: "d28ca95bbbbb"}
|
|
d.TrunkNow = "690966dccccccccc"
|
|
case EvLifted:
|
|
d.MergedAs, d.Lifted = "aaaa1111", "its group's composed check passed"
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkBuilding, Waits: true, WaitedFor: "mesh-delivery"}
|
|
case EvFailed:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkFailed}
|
|
case EvSuperseded:
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkSuperseded}
|
|
}
|
|
return d, f
|
|
}
|
|
|
|
func TestEveryRowOfTheTableIsTakenWhenItsGuardHoldsAndRefusedWhenNot(t *testing.T) {
|
|
for _, r := range Table {
|
|
for _, from := range r.From {
|
|
d, f := aDeliveryFor(r)
|
|
d.State = from
|
|
got, err := Apply(d, r.Event, r.Act, f)
|
|
if err != nil {
|
|
t.Errorf("%s —%s→ %s was refused with its guard holding: %v", orNothing(from), r.Event, r.To, err)
|
|
continue
|
|
}
|
|
if d.State != r.To || got.From != from || got.To != r.To || len(d.Transitions) != 1 {
|
|
t.Errorf("%s —%s→ left it %s, transition %+v", orNothing(from), r.Event, d.State, got)
|
|
}
|
|
// The guard not holding: a delivery with no facts.
|
|
bare := &Delivery{ID: "novox/app@bare", State: from, Check: &Verdict{Gate: "pass"}}
|
|
if r.Event == EvAccepted || r.Event == EvChecked {
|
|
bare.Check = nil
|
|
if r.Event == EvAccepted {
|
|
bare.Check = &Verdict{Gate: "fail"}
|
|
}
|
|
}
|
|
if r.Event == EvRefused {
|
|
bare.Check = &Verdict{Gate: "pass"}
|
|
}
|
|
if _, err := Apply(bare, r.Event, r.Act, Facts{Now: f.Now}); err == nil {
|
|
t.Errorf("%s —%s→ %s was taken with no fact for its guard", orNothing(from), r.Event, r.To)
|
|
} else if bare.State != from || len(bare.Transitions) != 0 {
|
|
t.Errorf("a refused transition moved the delivery to %s", bare.State)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) {
|
|
events := []Event{EvAnnounced, EvAppeared, EvAdopted, EvChecked, EvAccepted, EvRefused, EvRecheck, EvReask, EvNewHead,
|
|
EvClosed, EvMerged, EvMergedUnchecked, EvGo, EvHold, EvRelease, EvDone, EvFailed, EvSuperseded, EvStop,
|
|
EvHistory, EvRetire, EvCovered, EvLifted, EvRewalk, EvTrunkMoved}
|
|
for _, from := range append([]State{None}, AllStates...) {
|
|
for _, ev := range events {
|
|
for _, act := range []bool{false, true} {
|
|
if rowFor(from, ev, act) != nil {
|
|
continue
|
|
}
|
|
d := &Delivery{ID: "novox/app@c0ffee", State: from, MergedAs: "x", NewerHead: "y", Check: &Verdict{Gate: "pass"},
|
|
Walk: &WalkSeen{ID: "p", State: walkDone}, HeldWhy: "h", ClosedUnmerged: true, Number: 1, Commit: "c"}
|
|
_, err := Apply(d, ev, act, Facts{Now: time.Now(), Why: "w", By: "b"})
|
|
var refused ErrRefused
|
|
if !errors.As(err, &refused) || refused.Event != ev || refused.From != from {
|
|
t.Errorf("%s —%s (act %v)→ was not refused by name: %v", orNothing(from), ev, act, err)
|
|
}
|
|
if d.State != from {
|
|
t.Errorf("a pair the table does not hold moved %s to %s", orNothing(from), d.State)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The rules the table is for, read off the table.
|
|
func TestTheTableKeepsItsRules(t *testing.T) {
|
|
for _, r := range Table {
|
|
for _, from := range r.From {
|
|
// Nothing leaves a final state.
|
|
if from.Final() {
|
|
t.Errorf("%s is final and the table leaves it by %s", from, r.Event)
|
|
}
|
|
// Nothing goes back before the trunk once a delivery is on it.
|
|
if (from == Published || from == Delivering || from == Held) &&
|
|
(r.To == Proposed || r.To == Checked || r.To == Ready || r.To == Rejected) {
|
|
t.Errorf("%s goes back to %s by %s", from, r.To, r.Event)
|
|
}
|
|
}
|
|
// The trunk rule: only a ready delivery is published, by its merge — or a held one published again when
|
|
// what held it is gone (issue 316); nothing off the trunk delivers.
|
|
// And a merge whose walk carried a later commit for it and failed waits there for a walk of its own (ADR 0276).
|
|
if r.To == Published && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) &&
|
|
!(r.Event == EvLifted && len(r.From) == 1 && r.From[0] == Held) &&
|
|
!(r.Event == EvRewalk && len(r.From) == 1 && r.From[0] == Delivering) {
|
|
t.Errorf("%v —%s→ published: only a ready delivery's merge publishes", r.From, r.Event)
|
|
}
|
|
// Nothing a person did not decide ends a delivery as stopped or failed by observation from held.
|
|
if contains(r.From, Held) && !r.Act && (r.To == Stopped || r.To == Failed) {
|
|
t.Errorf("held —%s→ %s by observation", r.Event, r.To)
|
|
}
|
|
if r.To == Delivering {
|
|
for _, from := range r.From {
|
|
if from == Proposed || from == Checked || from == Ready || from == Rejected {
|
|
t.Errorf("%s delivers by %s without reaching the trunk", from, r.Event)
|
|
}
|
|
}
|
|
}
|
|
// Held is left only by a person or a walk that started on a word that was not this owner's.
|
|
if contains(r.From, Held) && r.To == Delivering && r.Event == EvRelease && !r.Act {
|
|
t.Error("a held delivery is released by observation")
|
|
}
|
|
}
|
|
// Every state that is not final has a bound or is the operator's to wait in, and every H2 transition is
|
|
// a row the table holds from that state: observed once the walk's record is read again, or H2's own ask.
|
|
for state, b := range Bounds {
|
|
for _, ev := range b.H2 {
|
|
if rowFor(state, ev, false) == nil && rowFor(state, ev, true) == nil {
|
|
t.Errorf("H2 may take %s from %s, which the table does not hold", ev, state)
|
|
}
|
|
}
|
|
}
|
|
// H2 re-asks a check once: a second re-ask is refused by the table itself.
|
|
d0 := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, CheckAsk: &CheckAsk{ID: "check-1", Reasked: 1}}
|
|
if _, err := Apply(d0, EvReask, true, Facts{Now: time.Now(), By: byH2, Why: "stalled"}); err == nil {
|
|
t.Fatal("a check re-asked once was re-asked again")
|
|
}
|
|
// A ready delivery off the trunk is refused publication, whatever else is true of it.
|
|
d := &Delivery{ID: "novox/app@c0", State: Ready, Walk: &WalkSeen{ID: "plan-x", State: walkRolling}}
|
|
if _, err := Apply(d, EvMerged, false, Facts{Now: time.Now()}); err == nil {
|
|
t.Fatal("a commit not on the trunk was published")
|
|
}
|
|
}
|
|
|
|
func TestTheMachineStepTable(t *testing.T) {
|
|
for _, c := range []struct {
|
|
from, to StepState
|
|
may bool
|
|
}{
|
|
{"", StepSent, true}, {StepSent, StepJudging, true}, {StepJudging, StepPassed, true},
|
|
{StepJudging, StepFailed, true}, {StepFailed, StepRolledBack, true}, {StepSent, StepFailed, true},
|
|
{StepPassed, StepFailed, false}, {StepRolledBack, StepPassed, false}, {StepPassed, StepSent, false},
|
|
{StepJudging, StepSent, false}, {StepFailed, StepPassed, false},
|
|
} {
|
|
if StepMay(c.from, c.to) != c.may {
|
|
t.Errorf("%q → %q: may %v, wanted %v", c.from, c.to, !c.may, c.may)
|
|
}
|
|
}
|
|
}
|
|
|
|
// settle stands a delivery where its facts put it, whatever order they came in.
|
|
func TestSettleIsTheOrderOfTheFactsNotOfTheirArrival(t *testing.T) {
|
|
now := time.Now()
|
|
d := &Delivery{ID: "novox/app@c0", Number: 1, Commit: "c0"}
|
|
if _, err := Apply(d, EvAnnounced, false, Facts{Now: now}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The verdict, the merge and a finished walk, all known before anything settled.
|
|
d.Check = &Verdict{Gate: "pass"}
|
|
d.MergedAs = "m1"
|
|
d.Walk = &WalkSeen{ID: "plan-1", State: walkDone}
|
|
ts := settle(d, Facts{Now: now})
|
|
var path []State
|
|
for _, t := range ts {
|
|
path = append(path, t.To)
|
|
}
|
|
want := []State{Checked, Ready, Published, Delivering, Delivered}
|
|
if len(path) != len(want) {
|
|
t.Fatalf("settled through %v, wanted %v", path, want)
|
|
}
|
|
for i := range want {
|
|
if path[i] != want[i] {
|
|
t.Fatalf("settled through %v, wanted %v", path, want)
|
|
}
|
|
}
|
|
}
|