mesh/merge-gate pass: builds 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 batch's walk the controller judged to move nothing is done when it is cut, and a merge it carried stayed published waiting for a start that cannot come. Its walk's record now settles it: delivered or covered when that walk built its modules, covered when a later delivered merge carried them (ADR 0270), and otherwise stopped with why once the cover test says none did.
255 lines
10 KiB
Go
255 lines
10 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 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}
|
|
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)
|
|
}
|
|
}
|
|
}
|