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 delivered
Two catalogue merges 18 s apart were walked by one walk at the later commit, which superseded the first; this owner tied that walk to the dunst delivery the supersession had ended and to no other, so nothing gave it its word and claude-code was held for a person with a reason naming neither cause. A delivery now follows the newest walk that answers its merge: one naming it (delivery.merges, the controller's batches), its own commit, the walk that took its own over (taken_over_by, or the superseded note), or, from a controller that names no merges, an open walk of its branch made for a later merge, moving all it moves, whose commit the forge says contains it. Only its own walk's record moves an ended delivery. The word is asked once per walk, when every delivery it carries is at its turn, a group's members on one walk together. An earlier merge carried by a later commit ends covered with the walk, and waits for a walk of its own when it fails. A merge no walk answers is held with the true reason and listed at once; a waiting walk no live delivery holds is listed as unheld; a merge in an assembling or queued batch is shown, not held. Replay R362 fails before.
576 lines
24 KiB
Go
576 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The holder, end to end over fakes (novox/hq ADR 0239 *How it is checked*): a pull request's head proposed,
|
|
// checked and ready; merged, published, let go, delivering and delivered; a gate that fails, put back and
|
|
// failed; a newer head superseding; a restart resuming; the trunk rule; and every transition kept before
|
|
// it is said, noted on the commit and shown on the pull request.
|
|
|
|
const (
|
|
head = "aaaaaaaaaaaa1111"
|
|
merge = "mmmmmmmmmmmm9999"
|
|
)
|
|
|
|
func TestAPullRequestsHeadIsProposedCheckedAndReady(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
id := IDOf("novox/app", head)
|
|
if w.state(id) != Proposed {
|
|
t.Fatalf("a head is %s", w.state(id))
|
|
}
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
d := w.delivery(id)
|
|
if d.State != Ready || d.Plan == nil || d.Plan.Summary != "builds app → anchor" {
|
|
t.Fatalf("a passing verdict left it %s with plan %+v", d.State, d.Plan)
|
|
}
|
|
var path []State
|
|
for _, tr := range d.Transitions {
|
|
path = append(path, tr.To)
|
|
}
|
|
if strings.Join(statesOf(path), ",") != "proposed,checked,ready" {
|
|
t.Fatalf("its transitions are %v", path)
|
|
}
|
|
w.settleAll()
|
|
notes := w.forge.notesOn(head)
|
|
if len(notes) != 3 || !strings.Contains(notes[2], "checked -> ready") {
|
|
t.Fatalf("the head's note is %q", notes)
|
|
}
|
|
if !strings.Contains(w.forge.statuses["novox/app@"+head+" mesh/delivery"], "success ready") {
|
|
t.Fatalf("the head's status is %q", w.forge.statuses)
|
|
}
|
|
if v := w.forge.views["novox/app#7"]; !strings.Contains(v, viewMarker) || !strings.Contains(v, "**ready**") {
|
|
t.Fatalf("the pull request's view is %q", v)
|
|
}
|
|
// A failing verdict for another head rejects it, and a newer head supersedes the older.
|
|
w.h.PullUpdated(pr("novox/app", 7, "bbbbbbbbbbbb2222", "feat/x"))
|
|
if w.state(id) != Superseded {
|
|
t.Fatalf("a newer head left the older %s", w.state(id))
|
|
}
|
|
w.h.Checked(verdict("novox/app", 7, "bbbbbbbbbbbb2222", "fail"))
|
|
if s := w.state(IDOf("novox/app", "bbbbbbbbbbbb2222")); s != Rejected {
|
|
t.Fatalf("a failing verdict left it %s", s)
|
|
}
|
|
}
|
|
|
|
func statesOf(ss []State) []string {
|
|
var out []string
|
|
for _, s := range ss {
|
|
out = append(out, string(s))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A merge publishes; the walk waits for this owner's word; the word is given; the walk's record moves the
|
|
// delivery through its machines to delivered — and the note on the merge commit says what was executed.
|
|
func TestAMergeIsPublishedLetGoDeliveringAndDelivered(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
// The walk is heard before the merge: it waits for its delivery.
|
|
walk := w.ctl.put(aWalk("plan-1", "novox/app", merge, true))
|
|
w.h.WalkMoved(walk)
|
|
if w.state(id) != Ready {
|
|
t.Fatalf("a walk for a commit no merge was heard of moved the delivery to %s", w.state(id))
|
|
}
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
if w.state(id) != Published {
|
|
t.Fatalf("a merge with its walk open left it %s", w.state(id))
|
|
}
|
|
w.settleAll()
|
|
if len(w.ctl.delivers) != 1 || w.ctl.delivers[0] != "plan-1" {
|
|
t.Fatalf("its walk was let go %v", w.ctl.delivers)
|
|
}
|
|
if w.state(id) != Delivering {
|
|
t.Fatalf("let go, it is %s", w.state(id))
|
|
}
|
|
// The first machine sent, judged, passed; the rest sent; the walk done.
|
|
sent := time.Date(2026, 10, 6, 12, 40, 0, 0, time.UTC)
|
|
walk = w.ctl.walk("plan-1")
|
|
walk.State = walkRolling
|
|
walk.Modules["app"] = &WalkModule{State: "built", First: []string{"anchor"}, FirstAt: &sent, Build: "build-1",
|
|
Gate: &WalkGate{Machines: []string{"anchor"}, Since: &sent, Passes: 1}}
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
if d := w.delivery(id); len(d.Steps) != 1 || d.Steps[0].State != StepJudging {
|
|
t.Fatalf("judging on its first machine reads %+v", d.Steps)
|
|
}
|
|
judged := sent.Add(3 * time.Minute)
|
|
walk.Modules["app"].Gate.Verdict, walk.Modules["app"].Gate.JudgedAt = "passed", &judged
|
|
walk.Modules["app"].SentAt = &judged
|
|
walk.State = walkDone
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
d := w.delivery(id)
|
|
if d.State != Delivered {
|
|
t.Fatalf("a walk done left it %s", d.State)
|
|
}
|
|
if len(d.Steps) != 2 || d.Steps[0].State != StepPassed || d.Steps[1].Machine != "the rest running it" {
|
|
t.Fatalf("its machine steps are %+v", d.Steps)
|
|
}
|
|
w.settleAll()
|
|
onMerge := w.forge.notesOn(merge)
|
|
if len(onMerge) == 0 || !strings.Contains(strings.Join(onMerge, "\n"), "executed (walk plan-1): app on anchor passed") {
|
|
t.Fatalf("the merge commit's note is %q", onMerge)
|
|
}
|
|
// Said on the bus, kept first: every transition emitted once.
|
|
n := 0
|
|
for _, s := range w.said {
|
|
if s["id"] == id {
|
|
n++
|
|
}
|
|
}
|
|
if n != len(d.Transitions) {
|
|
t.Fatalf("%d transitions, %d said", len(d.Transitions), n)
|
|
}
|
|
}
|
|
|
|
// A first machine's gate fails: what it carried is put back (the controller's), the step is rolled back,
|
|
// and the delivery failed — never delivered.
|
|
func TestAFailedGateIsRolledBackAndTheDeliveryFails(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-1", "novox/app", merge, true)))
|
|
w.settleAll()
|
|
sent := time.Date(2026, 10, 6, 12, 40, 0, 0, time.UTC)
|
|
walk := w.ctl.walk("plan-1")
|
|
walk.State = walkRolling
|
|
walk.Modules["app"] = &WalkModule{State: "built", First: []string{"anchor"}, FirstAt: &sent,
|
|
Gate: &WalkGate{Machines: []string{"anchor"}, Since: &sent}}
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
at := sent.Add(10 * time.Minute)
|
|
walk.Modules["app"].Gate.Verdict, walk.Modules["app"].Gate.Why, walk.Modules["app"].Gate.JudgedAt =
|
|
"failed", "its tools were not served", &at
|
|
walk.Modules["app"].Gate.Rollback = "rolled-back"
|
|
walk.State = walkFailed
|
|
walk.Note = "app failed its gate on anchor"
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
d := w.delivery(id)
|
|
if d.State != Failed || len(d.Steps) != 1 || d.Steps[0].State != StepRolledBack {
|
|
t.Fatalf("a failed gate left it %s, steps %+v", d.State, d.Steps)
|
|
}
|
|
// An older record arriving late changes nothing; a step the table does not hold is refused.
|
|
w.h.WalkMoved(aWalk("plan-1", "novox/app", merge, false))
|
|
if w.state(id) != Failed {
|
|
t.Fatal("an older record of the walk moved a final delivery")
|
|
}
|
|
}
|
|
|
|
// A walk superseded that names no walk taking it over (a record from before ADR 0276): superseded, from
|
|
// published or delivering. One that names its successor is followed (TestADeliveryFollowsTheWalkThatTookItsOver).
|
|
func TestANewerWalkSupersedesAnOlderDelivery(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
walk := aWalk("plan-1", "novox/app", merge, true)
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
walk.State = walkSuperseded
|
|
walk.Note = "superseded"
|
|
w.ctl.down = true // its word could not be given meanwhile
|
|
w.settleAll()
|
|
w.h.WalkMoved(w.ctl.put(walk))
|
|
if s := w.state(id); s != Superseded {
|
|
t.Fatalf("its walk superseded left it %s", s)
|
|
}
|
|
}
|
|
|
|
// The trunk rule: a ready delivery whose commit never reached the trunk is never published, whatever walks
|
|
// are heard of; and a walk for a commit no delivery landed as is not taken as its.
|
|
func TestACommitOffTheTrunkIsNeverPublished(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-9", "novox/app", head, true))) // a walk of the head itself: no merge
|
|
w.settleAll()
|
|
if s := w.state(id); s != Ready {
|
|
t.Fatalf("an off-trunk head is %s", s)
|
|
}
|
|
if len(w.ctl.delivers) != 0 {
|
|
t.Fatalf("a walk was let go for a commit off the trunk: %v", w.ctl.delivers)
|
|
}
|
|
}
|
|
|
|
// A restart resumes: a new holder over the same state stands every delivery where it was, reads the walks it
|
|
// missed back from the controller, and goes on.
|
|
func TestARestartResumesFromTheState(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.ctl.put(aWalk("plan-1", "novox/app", merge, true))
|
|
if err := w.h.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.settleAll()
|
|
if s := w.state(id); s != Delivering {
|
|
t.Fatalf("before the restart it is %s", s)
|
|
}
|
|
// While it is down, the walk finishes and nobody says so.
|
|
walk := w.ctl.walk("plan-1")
|
|
walk.State = walkDone
|
|
w.ctl.put(walk)
|
|
|
|
again := newWorldOver(t, w.store, w.ctl, w.forge, w.now.Add(time.Hour))
|
|
if s := again.state(id); s != Delivering {
|
|
t.Fatalf("read back, it is %s", s)
|
|
}
|
|
if err := again.h.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := again.state(id); s != Delivered {
|
|
t.Fatalf("the walk it missed was not read back: %s", s)
|
|
}
|
|
if len(w.ctl.delivers) != 1 {
|
|
t.Fatalf("a restarted holder let the walk go again: %v", w.ctl.delivers)
|
|
}
|
|
}
|
|
|
|
// Kept before it is said: with the state unreachable, nothing is emitted or noted; once it is kept, all of it.
|
|
func TestATransitionIsKeptBeforeItIsSaid(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.store.failing = true
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.settleAll()
|
|
if len(w.said) != 0 || len(w.forge.notesOn(head)) != 0 {
|
|
t.Fatalf("said before it was kept: %v %v", w.said, w.forge.notesOn(head))
|
|
}
|
|
w.store.failing = false
|
|
w.settleAll()
|
|
if len(w.said) != 1 || len(w.forge.notesOn(head)) != 1 {
|
|
t.Fatalf("once kept, said %d and noted %d", len(w.said), len(w.forge.notesOn(head)))
|
|
}
|
|
// A forge that is away is asked again; a note is appended once however often it is asked.
|
|
w.forge.down = true
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.settleAll()
|
|
if len(w.forge.notesOn(head)) != 1 {
|
|
t.Fatal("noted while the forge was away")
|
|
}
|
|
w.forge.down = false
|
|
w.later(time.Hour) // each failure put its next try later: past every one of them
|
|
w.settleAll()
|
|
w.settleAll()
|
|
if notes := w.forge.notesOn(head); len(notes) != 3 {
|
|
t.Fatalf("after the forge came back the note has %d line(s): %q", len(notes), notes)
|
|
}
|
|
if d := w.delivery(IDOf("novox/app", head)); len(d.Owed) != 0 {
|
|
t.Fatalf("still owed: %+v", d.Owed)
|
|
}
|
|
}
|
|
|
|
// At the switch: a walk already running is adopted as a delivery that is delivering; a waiting walk whose pull
|
|
// request this owner never heard of waits for a person, and a person's release lets it go.
|
|
func TestTheSwitchAdoptsRunningWalksAndHoldsUnknownWaitingOnes(t *testing.T) {
|
|
w := newWorld(t)
|
|
running := aWalk("plan-old", "novox/mesh-catalog", "cccccccccccc3333", false)
|
|
running.Created = w.now.Add(-time.Hour)
|
|
running.State = walkRolling
|
|
w.ctl.put(running)
|
|
waiting := aWalk("plan-new", "novox/app", "dddddddddddd4444", true)
|
|
waiting.Created = w.now.Add(time.Minute)
|
|
w.ctl.put(waiting)
|
|
if err := w.h.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.h.Tick()
|
|
if s := w.state(IDOf("novox/mesh-catalog", "cccccccccccc3333")); s != Delivering {
|
|
t.Fatalf("a running walk at the switch was adopted as %s", s)
|
|
}
|
|
if _, made := w.h.deliveries[IDOf("novox/app", "dddddddddddd4444")]; made {
|
|
t.Fatal("a new walk was made a delivery before its merge had time to be heard")
|
|
}
|
|
w.later(3 * time.Minute)
|
|
w.h.Tick()
|
|
held := IDOf("novox/app", "dddddddddddd4444")
|
|
if s := w.state(held); s != Held {
|
|
t.Fatalf("a waiting walk with no pull request is %s", s)
|
|
}
|
|
if _, err := w.h.Release(held, "", "jochen"); err == nil {
|
|
t.Fatal("released without why")
|
|
}
|
|
if _, err := w.h.Release(held, "checked by hand", "jochen"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := w.state(held); s != Delivering || len(w.ctl.delivers) != 1 {
|
|
t.Fatalf("released, it is %s and let go %v", s, w.ctl.delivers)
|
|
}
|
|
}
|
|
|
|
// mesh-delivery's controller down: nothing is lost — its word is asked again on the next tick.
|
|
func TestAControllerThatDoesNotAnswerIsAskedAgain(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-1", "novox/app", merge, true)))
|
|
w.ctl.down = true
|
|
w.settleAll()
|
|
if s := w.state(id); s != Published {
|
|
t.Fatalf("with the controller down it is %s", s)
|
|
}
|
|
w.ctl.down = false
|
|
w.settleAll()
|
|
if s := w.state(id); s != Delivering {
|
|
t.Fatalf("with the controller back it is %s", s)
|
|
}
|
|
}
|
|
|
|
// A merge before the check passed is held for a person; a merge this owner never heard the head of is made
|
|
// from the forge's word and published when the forge says its gate passed.
|
|
func TestAMergeWithoutAPassingCheckIsHeld(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "fail"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
if s := w.state(IDOf("novox/app", head)); s != Held {
|
|
t.Fatalf("a rejected head merged is %s", s)
|
|
}
|
|
// Never heard: the forge's statuses on its head decide.
|
|
m := merged("novox/lab", 3, "eeeeeeeeeeee5555", "ffffffffffff6666")
|
|
m.HeadChecks = map[string]string{"mesh/merge-gate": "success", "mesh/repo-check": "warning"}
|
|
m.Paths = []string{"README.md"}
|
|
w.h.PullMerged(m)
|
|
w.settleAll()
|
|
d := w.delivery(IDOf("novox/lab", "eeeeeeeeeeee5555"))
|
|
// The fake planner says it moves app; with no walk yet it waits published.
|
|
if d.State != Ready && d.State != Published {
|
|
t.Fatalf("a merge whose head passed by the forge's word is %s", d.State)
|
|
}
|
|
}
|
|
|
|
// A delivery group (ADR 0239 decision 4): two heads sharing a branch name across repositories, ordered by the
|
|
// controller, checked together, ready only when both are and their composition passed — and delivered in
|
|
// order, whatever order they merged in.
|
|
func TestAGroupIsOrderedCheckedTogetherAndDeliveredInOrder(t *testing.T) {
|
|
w := newWorld(t)
|
|
const ctlHead, catHead = "c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2"
|
|
ctl, cat := IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)
|
|
w.ctl.order = func(ms []Member) Order {
|
|
return Order{Order: []string{ctl, cat}, Pairs: []Pair{{Before: ctl, After: cat, Why: "version skew"}}}
|
|
}
|
|
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
|
|
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
|
|
if w.delivery(ctl).Group != "feat/x" || w.delivery(cat).Group != "feat/x" {
|
|
t.Fatalf("one branch name in two repositories made no group: %q %q", w.delivery(ctl).Group, w.delivery(cat).Group)
|
|
}
|
|
w.h.Checked(verdict("novox/mesh-controller", 1, ctlHead, "pass"))
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 0 {
|
|
t.Fatal("the group was checked composed before every member was ready")
|
|
}
|
|
w.h.Checked(verdict("novox/mesh-catalog", 2, catHead, "pass"))
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 1 || w.ctl.checks[0] != "feat/x:"+ctl+","+cat {
|
|
t.Fatalf("the composed check asked %v, wanted the members in order", w.ctl.checks)
|
|
}
|
|
if state, _ := GroupState(w.h.groups["feat/x"], w.h.membersOf(w.h.groups["feat/x"])); state != "checking" {
|
|
t.Fatalf("before its composed verdict the group is %s", state)
|
|
}
|
|
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
|
|
w.settleAll()
|
|
if state, _ := GroupState(w.h.groups["feat/x"], w.h.membersOf(w.h.groups["feat/x"])); state != "ready" {
|
|
t.Fatalf("composed and ready, the group is %s", state)
|
|
}
|
|
if !strings.HasPrefix(w.forge.statuses["novox/mesh-catalog@"+catHead+" mesh/delivery-group"], "success") {
|
|
t.Fatalf("the group's status on a member's head: %v", w.forge.statuses)
|
|
}
|
|
|
|
// The catalogue merges first; its walk waits — the controller has not been delivered.
|
|
w.h.PullMerged(merged("novox/mesh-catalog", 2, catHead, "m2m2m2m2m2m2"))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-cat", "novox/mesh-catalog", "m2m2m2m2m2m2", true)))
|
|
w.settleAll()
|
|
if w.state(cat) != Published || len(w.ctl.delivers) != 0 {
|
|
t.Fatalf("a member merged before the one it goes after was let go: %s %v", w.state(cat), w.ctl.delivers)
|
|
}
|
|
// The controller merges: its walk is on the controller's own path, started at the merge.
|
|
w.h.PullMerged(merged("novox/mesh-controller", 1, ctlHead, "m1m1m1m1m1m1"))
|
|
ctlWalk := w.ctl.put(aWalk("plan-ctl", "novox/mesh-controller", "m1m1m1m1m1m1", false))
|
|
w.h.WalkMoved(ctlWalk)
|
|
w.settleAll()
|
|
if w.state(ctl) != Delivering || len(w.ctl.delivers) != 0 {
|
|
t.Fatalf("the controller's member is %s, let go %v", w.state(ctl), w.ctl.delivers)
|
|
}
|
|
ctlWalk.State = walkDone
|
|
w.h.WalkMoved(w.ctl.put(ctlWalk))
|
|
w.settleAll()
|
|
if w.state(ctl) != Delivered || len(w.ctl.delivers) != 1 || w.ctl.delivers[0] != "plan-cat" {
|
|
t.Fatalf("once the controller was delivered, the catalogue was not let go: %s %v", w.state(ctl), w.ctl.delivers)
|
|
}
|
|
if w.state(cat) != Delivering {
|
|
t.Fatalf("the catalogue's member is %s", w.state(cat))
|
|
}
|
|
}
|
|
|
|
// A member that fails stops every member after it, naming it; what was delivered before it stays.
|
|
func TestAFailedMemberStopsTheMembersAfterItAndLeavesThoseBefore(t *testing.T) {
|
|
w := newWorld(t)
|
|
heads := map[string]string{"novox/a": "a1a1a1a1a1a1a1", "novox/b": "b2b2b2b2b2b2b2", "novox/c": "c3c3c3c3c3c3c3"}
|
|
ids := map[string]string{}
|
|
for i, repo := range []string{"novox/a", "novox/b", "novox/c"} {
|
|
ids[repo] = IDOf(repo, heads[repo])
|
|
w.h.PullUpdated(pr(repo, i+1, heads[repo], "feat/y", "x"))
|
|
w.h.Checked(verdict(repo, i+1, heads[repo], "pass"))
|
|
}
|
|
w.settleAll()
|
|
w.h.Checked(CheckedEvent{Group: "feat/y", ID: "check-1", Verdict: "pass", Summary: "compose"})
|
|
for i, repo := range []string{"novox/a", "novox/b", "novox/c"} {
|
|
mergeCommit := strings.Repeat(string(rune('p'+i)), 12)
|
|
w.h.PullMerged(merged(repo, i+1, heads[repo], mergeCommit))
|
|
w.h.WalkMoved(w.ctl.put(aWalk("plan-"+repo[6:], repo, mergeCommit, true)))
|
|
}
|
|
w.settleAll()
|
|
a := w.ctl.walk("plan-a")
|
|
a.State = walkDone
|
|
w.h.WalkMoved(w.ctl.put(a))
|
|
w.settleAll()
|
|
b := w.ctl.walk("plan-b")
|
|
if b.Delivery == nil || b.Delivery.Go == nil {
|
|
t.Fatalf("b was not let go after a was delivered: %v", w.ctl.delivers)
|
|
}
|
|
b.State = walkFailed
|
|
b.Note = "b failed its gate on anchor"
|
|
w.h.WalkMoved(w.ctl.put(b))
|
|
w.settleAll()
|
|
if w.state(ids["novox/a"]) != Delivered || w.state(ids["novox/b"]) != Failed || w.state(ids["novox/c"]) != Stopped {
|
|
t.Fatalf("a %s, b %s, c %s", w.state(ids["novox/a"]), w.state(ids["novox/b"]), w.state(ids["novox/c"]))
|
|
}
|
|
c := w.delivery(ids["novox/c"])
|
|
if why := c.Transitions[len(c.Transitions)-1].Why; !strings.Contains(why, ids["novox/b"]) {
|
|
t.Fatalf("c's stop does not name the member that failed: %q", why)
|
|
}
|
|
if len(w.ctl.stops) != 1 || w.ctl.stops[0] != "plan-c" {
|
|
t.Fatalf("c's walk was not ended: %v", w.ctl.stops)
|
|
}
|
|
if state, _ := GroupState(w.h.groups["feat/y"], w.h.membersOf(w.h.groups["feat/y"])); state != "failed" {
|
|
t.Fatalf("the group is %s", state)
|
|
}
|
|
}
|
|
|
|
// A group whose declared order contradicts its inferred one is rejected, and checked never.
|
|
func TestAGroupWithACycleIsRejected(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.ctl.order = func(ms []Member) Order { return Order{Cycle: []string{ms[0].ID, ms[1].ID}} }
|
|
w.h.PullUpdated(pr("novox/a", 1, "a1a1a1a1a1a1a1", "feat/z"))
|
|
w.h.PullUpdated(pr("novox/b", 2, "b2b2b2b2b2b2b2", "feat/z"))
|
|
w.h.Checked(verdict("novox/a", 1, "a1a1a1a1a1a1a1", "pass"))
|
|
w.h.Checked(verdict("novox/b", 2, "b2b2b2b2b2b2b2", "pass"))
|
|
w.settleAll()
|
|
g := w.h.groups["feat/z"]
|
|
if state, why := GroupState(g, w.h.membersOf(g)); state != "rejected" || !strings.Contains(why, "contradicts") {
|
|
t.Fatalf("a cycle left the group %s: %s", state, why)
|
|
}
|
|
if len(w.ctl.checks) != 0 {
|
|
t.Fatal("a group with a cycle was checked")
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 309: a group whose rules order one pair both ways at one rank is refused with both rules
|
|
// and how to declare the order; a pair a rule won by precedence says what it won over, and the group is
|
|
// checked as any other.
|
|
func TestAGroupSaysWhichOrderRuleWonOrWhyNoneDid(t *testing.T) {
|
|
w := newWorld(t)
|
|
said := "novox/a@a1a1a1a1a1a1 and novox/b@b2b2b2b2b2b2: built by puts novox/a@a1a1a1a1a1a1 first, built by puts " +
|
|
"novox/b@b2b2b2b2b2b2 first; neither outranks the other: declare the order with a line `after: novox/a` in " +
|
|
"novox/b#2's description, or `after: novox/b` in novox/a#1's"
|
|
w.ctl.order = func(ms []Member) Order {
|
|
return Order{Cycle: []string{ms[0].ID, ms[1].ID}, Contradictions: []Contradiction{{
|
|
Members: []string{ms[0].ID, ms[1].ID}, Said: said,
|
|
Rules: []Pair{{Before: ms[0].ID, After: ms[1].ID, Why: "built by"}, {Before: ms[1].ID, After: ms[0].ID, Why: "built by"}}}}}
|
|
}
|
|
w.h.PullUpdated(pr("novox/a", 1, "a1a1a1a1a1a1a1", "feat/z"))
|
|
w.h.PullUpdated(pr("novox/b", 2, "b2b2b2b2b2b2b2", "feat/z"))
|
|
w.h.Checked(verdict("novox/a", 1, "a1a1a1a1a1a1a1", "pass"))
|
|
w.h.Checked(verdict("novox/b", 2, "b2b2b2b2b2b2b2", "pass"))
|
|
w.settleAll()
|
|
g := w.h.groups["feat/z"]
|
|
if state, why := GroupState(g, w.h.membersOf(g)); state != "rejected" || !strings.Contains(why, said) {
|
|
t.Fatalf("a contradiction was said %s: %s", state, why)
|
|
}
|
|
if lines := w.h.Groups(false); len(lines) != 1 || len(lines[0].Contradictions) != 1 {
|
|
t.Fatalf("groups does not list the contradiction: %+v", lines)
|
|
}
|
|
|
|
// Resolved by precedence: ordered, the pair saying what it won over, and checked.
|
|
w2 := newWorld(t)
|
|
w2.ctl.order = func(ms []Member) Order {
|
|
return Order{Order: []string{ms[0].ID, ms[1].ID},
|
|
Pairs: []Pair{{Before: ms[0].ID, After: ms[1].ID, Why: "built by", Over: []string{"engine before controller"}}}}
|
|
}
|
|
w2.h.PullUpdated(pr("novox/a", 1, "a1a1a1a1a1a1a1", "feat/z"))
|
|
w2.h.PullUpdated(pr("novox/b", 2, "b2b2b2b2b2b2b2", "feat/z"))
|
|
w2.h.Checked(verdict("novox/a", 1, "a1a1a1a1a1a1a1", "pass"))
|
|
w2.h.Checked(verdict("novox/b", 2, "b2b2b2b2b2b2b2", "pass"))
|
|
w2.settleAll()
|
|
g = w2.h.groups["feat/z"]
|
|
if len(g.Cycle) > 0 || len(w2.ctl.checks) != 1 {
|
|
t.Fatalf("a group ordered by precedence was not checked: cycle %v, checks %v", g.Cycle, w2.ctl.checks)
|
|
}
|
|
if got := g.Pairs[0].orderSaid(); !strings.HasSuffix(got, ": built by, over engine before controller") {
|
|
t.Fatalf("the pair is said %q", got)
|
|
}
|
|
}
|
|
|
|
// A cycle through members with no contradiction of its own names the pairs in it.
|
|
func TestACycleWithoutAContradictionNamesItsPairs(t *testing.T) {
|
|
g := &Group{Cycle: []string{"a", "b", "c"}, Pairs: []Pair{{Before: "a", After: "b", Why: "built by"},
|
|
{Before: "b", After: "c", Why: "declared"}, {Before: "c", After: "a", Why: "version skew"}}}
|
|
why := cycleSaid(g)
|
|
for _, w := range []string{"a before b: built by", "b before c: declared", "c before a: version skew", "`after: <repository>`"} {
|
|
if !strings.Contains(why, w) {
|
|
t.Errorf("the cycle does not say %q: %s", w, why)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stalled: a delivery past its state's bound is listed with what H2 may do; close takes only that.
|
|
func TestStalledAndCloseWorkFromTheTable(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
walk := w.ctl.put(aWalk("plan-1", "novox/app", merge, false))
|
|
w.h.WalkMoved(walk)
|
|
if w.state(id) != Delivering {
|
|
t.Fatalf("it is %s", w.state(id))
|
|
}
|
|
if _, err := w.h.Close(id, "too early"); err == nil {
|
|
t.Fatal("closed within its bound")
|
|
}
|
|
w.later(3 * time.Hour)
|
|
stalled := w.h.Stalled()
|
|
if len(stalled) != 1 || stalled[0].ID != id || !strings.Contains(stalled[0].H2, "close") {
|
|
t.Fatalf("stalled %+v", stalled)
|
|
}
|
|
if _, err := w.h.Close(id, "delivery stalled"); err == nil {
|
|
t.Fatal("closed with nothing in the walk's record saying it moved")
|
|
}
|
|
walk.State = walkDone
|
|
w.ctl.put(walk) // finished, and the event was missed
|
|
said, err := w.h.Close(id, "delivery stalled")
|
|
if err != nil || w.state(id) != Delivered {
|
|
t.Fatalf("H2's close left it %s: %v", w.state(id), err)
|
|
}
|
|
if !strings.Contains(said, "delivered") {
|
|
t.Fatalf("close says %q", said)
|
|
}
|
|
// Held is the operator's: H2 has no transition there.
|
|
w.h.PullUpdated(pr("novox/app", 8, "ffffffffffff0000", "feat/q"))
|
|
w.h.PullMerged(merged("novox/app", 8, "ffffffffffff0000", "eeeeeeeeeeee0000"))
|
|
w.later(48 * time.Hour)
|
|
if _, err := w.h.Close(IDOf("novox/app", "ffffffffffff0000"), "x"); err == nil || !strings.Contains(err.Error(), "operator's") {
|
|
t.Fatalf("H2 closed a held delivery: %v", err)
|
|
}
|
|
}
|