The router holds what an open walk touches and tells its result once; it can do so only if plan-moved names the walk's start and end, the modules and machines it moves, what a moved tool runner or node-engine carries there, and how long to hold.
131 lines
5.2 KiB
Go
131 lines
5.2 KiB
Go
package inventory
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A walk is said started on the save that finds it live, ended on the save that finds it ended after it started,
|
|
// and neither on any other save (novox/hq ADR 0308 decision 1).
|
|
func TestAWalkIsSaidStartedOnceAndEndedOnce(t *testing.T) {
|
|
now := time.Date(2026, 10, 12, 11, 0, 0, 0, time.UTC)
|
|
cut := now.Add(-time.Second)
|
|
p := Plan{ID: "plan-1", State: PlanBuilding, Tiers: [][]string{{"messenger"}},
|
|
Modules: map[string]*PlanModule{"messenger": {}}, Times: &PlanTimes{Cut: &cut, Class: ClassLeaf}}
|
|
p.markWalk(now)
|
|
if p.WalkSaid != WalkStarted || p.Times.Started == nil || !p.Times.Started.Equal(now) {
|
|
t.Fatalf("a live walk's first save starts it: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
p.State = PlanRolling
|
|
p.markWalk(now.Add(time.Minute))
|
|
if p.WalkSaid != "" || !p.Times.Started.Equal(now) {
|
|
t.Fatalf("a later save says nothing and keeps the start: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
p.State = PlanDone
|
|
p.markWalk(now.Add(2 * time.Minute))
|
|
if p.WalkSaid != WalkEnded || p.Times.Ended == nil || p.Outcome() != WalkDelivered {
|
|
t.Fatalf("the save that finds it done ends it, delivered: %q %+v %q", p.WalkSaid, p.Times, p.Outcome())
|
|
}
|
|
p.markWalk(now.Add(3 * time.Minute))
|
|
if p.WalkSaid != "" {
|
|
t.Fatalf("an ended walk saved again says nothing: %q", p.WalkSaid)
|
|
}
|
|
// Retried, it walks again: a new start.
|
|
p.State = PlanBuilding
|
|
p.markWalk(now.Add(4 * time.Minute))
|
|
if p.WalkSaid != WalkStarted || p.Times.Ended != nil || !p.Times.Started.Equal(now.Add(4*time.Minute)) {
|
|
t.Fatalf("a retried walk starts again: %q %+v", p.WalkSaid, p.Times)
|
|
}
|
|
}
|
|
|
|
// What is never said: a batch, a walk waiting for its word, a walk that ends without having started, and a walk
|
|
// kept before its times were. A release plan is said.
|
|
func TestWhatIsNotAWalkStartIsNotSaid(t *testing.T) {
|
|
now := time.Now()
|
|
for name, p := range map[string]Plan{
|
|
"a batch": {State: PlanAssembling, Times: &PlanTimes{}},
|
|
"waiting for a word": {State: PlanBuilding, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery"}},
|
|
"ended, never begun": {State: PlanDone, Times: &PlanTimes{}},
|
|
"kept before times": {State: PlanBuilding},
|
|
"stopped while waits": {State: PlanFailed, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery", Stopped: "x"}},
|
|
} {
|
|
p.markWalk(now)
|
|
if p.WalkSaid != "" {
|
|
t.Errorf("%s: said %q", name, p.WalkSaid)
|
|
}
|
|
}
|
|
r := Plan{State: PlanRolling, Release: &PlanRelease{Order: []string{"ace"}}}
|
|
r.markWalk(now)
|
|
if r.WalkSaid != WalkStarted || r.Times == nil || r.Times.Started == nil || r.Times.Class != "" {
|
|
t.Fatalf("a release plan is said started, its times holding only that: %q %+v", r.WalkSaid, r.Times)
|
|
}
|
|
// Let go, the walk starts.
|
|
go1 := now
|
|
w := Plan{State: PlanBuilding, Times: &PlanTimes{}, Delivery: &PlanDelivery{Awaits: "mesh-delivery", Go: &go1}}
|
|
w.markWalk(now)
|
|
if w.WalkSaid != WalkStarted {
|
|
t.Fatalf("a walk let go starts: %q", w.WalkSaid)
|
|
}
|
|
}
|
|
|
|
// An ended walk's outcome: delivered, failed, or stopped by a person or the delivery's owner, or by a newer walk.
|
|
func TestAWalksOutcome(t *testing.T) {
|
|
for want, p := range map[string]Plan{
|
|
WalkDelivered: {State: PlanDone},
|
|
WalkFailed: {State: PlanFailed},
|
|
WalkStopped: {State: PlanFailed, Delivery: &PlanDelivery{Stopped: "mesh-delivery"}},
|
|
"": {State: PlanRolling},
|
|
} {
|
|
if got := p.Outcome(); got != want {
|
|
t.Errorf("%s: outcome %q, want %q", p.State, got, want)
|
|
}
|
|
}
|
|
if (Plan{State: PlanSuperseded}).Outcome() != WalkStopped {
|
|
t.Error("a superseded walk is stopped")
|
|
}
|
|
}
|
|
|
|
// The marks are kept with the walk, so a controller replaced mid-walk neither says its start again nor misses
|
|
// its end; and a save refused leaves the walk unmarked, so the save that is made says it.
|
|
func TestAWalksStartIsKeptAndARefusedSaveLeavesItUnsaid(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := t.Context()
|
|
cut := time.Now().UTC().Add(-time.Minute)
|
|
p := Plan{ID: "plan-308", Repository: "novox/mesh-catalog", Commit: "abc", Created: cut, State: PlanBuilding,
|
|
Tiers: [][]string{{"messenger"}}, Modules: map[string]*PlanModule{"messenger": {}},
|
|
Times: &PlanTimes{Cut: &cut, Class: ClassLeaf}}
|
|
var said []string
|
|
was := PlanSaved
|
|
PlanSaved = func(p Plan) { said = append(said, p.WalkSaid) }
|
|
t.Cleanup(func() { PlanSaved = was })
|
|
if err := inv.SavePlan(ctx, &p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := inv.PlanByID(ctx, p.ID)
|
|
if err != nil || back.Times == nil || back.Times.Started == nil || back.Times.Class != ClassLeaf {
|
|
t.Fatalf("the start is kept with the walk: %v %+v", err, back.Times)
|
|
}
|
|
// Read back by another controller and saved: not said again.
|
|
back.State = PlanRolling
|
|
if err := inv.SavePlan(ctx, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A stale copy ending it is refused, and unmarked.
|
|
stale := p
|
|
stale.Revision = 1
|
|
stale.State = PlanDone
|
|
if err := inv.SavePlan(ctx, &stale); err == nil {
|
|
t.Fatal("a stale save is refused")
|
|
}
|
|
if stale.WalkSaid != "" || stale.Times.Ended != nil {
|
|
t.Fatalf("a refused save is unmarked: %q %+v", stale.WalkSaid, stale.Times)
|
|
}
|
|
back.State = PlanDone
|
|
if err := inv.SavePlan(ctx, &back); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(said) != 3 || said[0] != WalkStarted || said[1] != "" || said[2] != WalkEnded {
|
|
t.Fatalf("said: %q, want started, nothing, ended", said)
|
|
}
|
|
}
|