Take no rebuild into a waiting walk, and ask what a tier left unasked (hq issue 754)
mesh/merge-gate head 9f072c110c14 merged into trunk 60cc9ec363df — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 9f072c110c14 merged into trunk 60cc9ec363df — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/merge-gate head 9f072c110c14 merged into trunk 60cc9ec363df — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 9f072c110c14 merged into trunk 60cc9ec363df — pass: its merge-check.sh passed
mesh/delivery delivered
A rebuild that landed while a walk waited for mesh-delivery's word was taken as the walk's build of that module; once let go, the walk asked a tier only when every module of it was unasked, so it never asked the rest and stood still. A walk now takes only the outcomes of builds it asked, and none while it waits; a rebuild does not join a waiting walk and says it runs beside it; and a tier asked in part is asked the rest, including a module held built that the walk never asked for, which is somebody else's build and not sent as the walk's.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// novox/hq issue 754: a walk waiting for its delivery's word takes no build's outcome, nor does a walk take one for
|
||||
// a module it never asked; asked under an id, it takes that id's alone.
|
||||
func TestAWalkTakesOnlyTheOutcomesOfWhatItAsked(t *testing.T) {
|
||||
asked := time.Date(2026, 10, 14, 23, 30, 0, 0, time.UTC)
|
||||
before := asked.Add(-time.Minute)
|
||||
waiting := inventory.Plan{Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery"}}
|
||||
letGo := inventory.Plan{Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery", Go: &asked}}
|
||||
own := &inventory.PlanModule{State: "asked", AskedAt: &asked, Build: "build-own"}
|
||||
old := &inventory.PlanModule{State: "asked", AskedAt: &asked}
|
||||
for _, c := range []struct {
|
||||
what string
|
||||
p inventory.Plan
|
||||
state *inventory.PlanModule
|
||||
at time.Time
|
||||
id string
|
||||
takes bool
|
||||
}{
|
||||
{"a rebuild while the walk waits", waiting, nil, asked, "build-rebuild", false},
|
||||
{"its own id while it waits", waiting, own, asked, "build-own", false},
|
||||
{"a module it never asked", letGo, nil, asked, "build-rebuild", false},
|
||||
{"a module it never asked, state kept empty", letGo, &inventory.PlanModule{}, asked, "build-rebuild", false},
|
||||
{"another id", letGo, own, asked, "build-rebuild", false},
|
||||
{"its own id", letGo, own, asked, "build-own", true},
|
||||
{"a plan from before ids, asked after it", letGo, old, asked.Add(time.Second), "x", true},
|
||||
{"a plan from before ids, asked before it", letGo, old, before, "x", false},
|
||||
{"a plan with no delivery, its own id", inventory.Plan{}, own, asked, "build-own", true},
|
||||
} {
|
||||
if got := takesOutcome(c.p, c.state, c.at, c.id); got != c.takes {
|
||||
t.Errorf("%s: takes %v, want %v", c.what, got, c.takes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 754: a tier partly asked asks the rest; a module held built that the plan never asked — the live
|
||||
// shape, a hand rebuild taken in during the wait, built after the plan was made — is asked again; one asked is kept.
|
||||
func TestATierPartlyAskedAsksTheRest(t *testing.T) {
|
||||
created := time.Date(2026, 10, 14, 23, 30, 0, 0, time.UTC)
|
||||
earlier, later := created.Add(-time.Hour), created.Add(7*time.Minute)
|
||||
p := inventory.Plan{Created: created, Tiers: [][]string{{"build-agent", "gitea", "route-proxy", "stale", "kept", "own"}},
|
||||
Modules: map[string]*inventory.PlanModule{
|
||||
"gitea": {},
|
||||
"route-proxy": {State: "built", BuiltAt: &later, Commit: "c51357f8"},
|
||||
"stale": {State: "built", BuiltAt: &earlier},
|
||||
"kept": {State: "asked", AskedAt: &created, Build: "build-kept"},
|
||||
"own": {State: "built", AskedAt: &created, BuiltAt: &later, Build: "build-own"},
|
||||
}}
|
||||
if got, want := unaskedOf(p), []string{"build-agent", "gitea", "route-proxy", "stale"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unasked %v, want %v", got, want)
|
||||
}
|
||||
if !askedInTier(p) {
|
||||
t.Fatal("a tier with a module asked under an id reads as never asked")
|
||||
}
|
||||
p.Modules["kept"], p.Modules["own"] = nil, nil
|
||||
if askedInTier(p) {
|
||||
t.Fatal("a tier the plan never asked reads as asked")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 754, as seen on 2026-10-14: a rebuild lands while the walk waits for its word; the walk is let go;
|
||||
// it asks every module of its tier, and completes it once those builds come in.
|
||||
func TestARebuildInTheWaitIsNotTheWalksBuild(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
asked := asksRecorded(t)
|
||||
twoTiers(t, open)
|
||||
created := time.Now().UTC().Add(-10 * time.Minute)
|
||||
plan := inventory.Plan{ID: "plan-754", Repository: "novox/a", Branch: "main", Commit: "c0ffee", Created: created,
|
||||
State: inventory.PlanBuilding, Tiers: [][]string{{"a", "b"}}, Modules: map[string]*inventory.PlanModule{},
|
||||
Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery"}}
|
||||
if err := open.inventory.SavePlan(ctx, &plan); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A rebuild of a, beside the waiting walk: not joined, and its outcome not taken.
|
||||
joined, said, err := joinAPlan(ctx, open, "a")
|
||||
if err != nil || joined || said == "" {
|
||||
t.Fatalf("a rebuild joined a waiting walk: %v %q %v", joined, said, err)
|
||||
}
|
||||
planBuilt(ctx, open, "a", "c0ffee", "", time.Now().UTC(), "build-rebuild")
|
||||
p, _ := open.inventory.PlanByID(ctx, plan.ID)
|
||||
if s := p.Modules["a"]; s != nil && s.State != "" {
|
||||
t.Fatalf("the waiting walk took a rebuild as its build: %+v", s)
|
||||
}
|
||||
if len(*asked) != 0 {
|
||||
t.Fatalf("a waiting walk asked %v", *asked)
|
||||
}
|
||||
|
||||
// Let go: both are asked, and their outcomes complete the tier.
|
||||
if _, err := letGo(ctx, open.inventory, plan.ID, "operator", "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
advancePlans(ctx, open)
|
||||
p, _ = open.inventory.PlanByID(ctx, plan.ID)
|
||||
for _, m := range []string{"a", "b"} {
|
||||
if s := p.Modules[m]; s == nil || s.State != "asked" || s.Build == "" {
|
||||
t.Fatalf("let go, %s is %+v", m, s)
|
||||
}
|
||||
}
|
||||
if len(*asked) != 2 {
|
||||
t.Fatalf("asked %v", *asked)
|
||||
}
|
||||
for _, m := range []string{"a", "b"} {
|
||||
id := p.Modules[m].Build
|
||||
at, _ := link.BuildAskedAt(id)
|
||||
planBuilt(ctx, open, m, "c0ffee", "", at, id)
|
||||
}
|
||||
advancePlans(ctx, open)
|
||||
if p, _ = open.inventory.PlanByID(ctx, plan.ID); p.Open() && p.Tier == 0 {
|
||||
t.Fatalf("the walk did not get past its tier: %s at %d, %q", p.State, p.Tier, p.Note)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user