mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestReplay336 (1.12s)
mesh/delivery superseded: a newer head of the same pull request
The condition and the plan named the verb in command-line form; the operator reaches it through the mesh MCP server, so it is written as that call. A rebuild of the same source moves nothing (issue 280), which the plan cannot know before the build, so its line says so.
160 lines
7.2 KiB
Go
160 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq issue 336: a send held because it would replace the bus outside its planned step waits for a
|
|
// person, so a person is told at once — what waits, behind which bus build, since when, and the verb that
|
|
// ends it — and the wait is not read as a walk running late.
|
|
|
|
// aBusOnAnchor is a new bus build waiting for its step on anchor: nats 88135ad0 there, 32307bd1 held.
|
|
func aBusOnAnchor() busPending {
|
|
return busPending{module: "nats", machines: []string{"anchor"}, from: map[string]string{"anchor": "88135ad0aaaa"},
|
|
to: "32307bd1bbbb", same: map[string]bool{}}
|
|
}
|
|
|
|
// refusedNote is the note a walk keeps when its send was refused for the bus, as advanceHeld writes it.
|
|
func refusedNote(b busPending, tier int) string {
|
|
held := "sending anchor would replace the bus (nats " + short(b.from["anchor"]) + " → " + short(b.to) +
|
|
"), which is a planned step: `bus upgrade --why …` snapshots its streams first and checks them after (novox/hq ADR 0236)"
|
|
return "tier " + string(rune('0'+tier)) + ": " + errBusWaits.Error() + ": " + held + " — tried again"
|
|
}
|
|
|
|
func TestASendHeldForTheBusStepIsFoundFromTheWalksItHolds(t *testing.T) {
|
|
now := time.Date(2026, 10, 8, 18, 30, 0, 0, time.UTC)
|
|
b := aBusOnAnchor()
|
|
walk := func(id, repo, note string, updated time.Time) inventory.Plan {
|
|
return inventory.Plan{ID: id, Repository: repo, Commit: "c0ffee001122", State: inventory.PlanRolling, Tier: 0,
|
|
Tiers: [][]string{{"mesh-host"}}, Modules: map[string]*inventory.PlanModule{"mesh-host": {}}, Note: note,
|
|
Updated: updated}
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
bus busPending
|
|
plans []inventory.Plan
|
|
first map[string]time.Time
|
|
want []string
|
|
since time.Time
|
|
}{
|
|
{"a walk refused for the bus", b,
|
|
[]inventory.Plan{walk("plan-1", "novox/mesh-host", refusedNote(b, 0), now.Add(-27*time.Minute))}, nil,
|
|
[]string{"plan-1"}, now.Add(-27 * time.Minute)},
|
|
{"refused earlier than its last save, as this controller saw it", b,
|
|
[]inventory.Plan{walk("plan-1", "novox/mesh-host", refusedNote(b, 0), now.Add(-5*time.Minute))},
|
|
map[string]time.Time{"plan-1": now.Add(-28 * time.Minute)}, []string{"plan-1"}, now.Add(-28 * time.Minute)},
|
|
{"a walk refused for another reason", b,
|
|
[]inventory.Plan{walk("plan-1", "novox/mesh-host", "tier 0: the build seat is paused — tried again", now)}, nil,
|
|
nil, time.Time{}},
|
|
{"refused for an older bus build than the one held now", func() busPending { o := b; o.to = "99999999cccc"; return o }(),
|
|
[]inventory.Plan{walk("plan-1", "novox/mesh-host", refusedNote(b, 0), now)}, nil, nil, time.Time{}},
|
|
{"the bus already runs the build held: its step started", func() busPending {
|
|
o := aBusOnAnchor()
|
|
o.from = map[string]string{"anchor": o.to}
|
|
return o
|
|
}(), []inventory.Plan{walk("plan-1", "novox/mesh-host", refusedNote(b, 0), now)}, nil, nil, time.Time{}},
|
|
{"a walk waiting for its delivery's word asks no send", b, func() []inventory.Plan {
|
|
p := walk("plan-1", "novox/mesh-host", refusedNote(b, 0), now)
|
|
p.Delivery = &inventory.PlanDelivery{Awaits: "mesh-delivery"}
|
|
return []inventory.Plan{p}
|
|
}(), nil, nil, time.Time{}},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := busWaitsOf(c.plans, c.bus, func(id string) time.Time { return c.first[id] })
|
|
var ids []string
|
|
for _, w := range got.waits {
|
|
ids = append(ids, w.plan)
|
|
}
|
|
if strings.Join(ids, ",") != strings.Join(c.want, ",") {
|
|
t.Fatalf("held for the bus: %v, want %v", ids, c.want)
|
|
}
|
|
if len(c.want) > 0 {
|
|
if !got.waits[0].since.Equal(c.since) {
|
|
t.Errorf("waiting since %s, want %s", got.waits[0].since, c.since)
|
|
}
|
|
if strings.Join(got.machines, ",") != "anchor" || got.to != b.to || got.module != "nats" {
|
|
t.Errorf("behind %+v", got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// **Said at once, not as lateness, and cleared when the step starts**: the walk held only for the bus raises
|
|
// the bus's condition on the first tick, before any tier bound, naming what waits, the bus build from and to,
|
|
// since when and `bus upgrade`, for the operator; S3 says nothing of it even past its bound; and the
|
|
// condition clears once the bus's machine runs the new build.
|
|
func TestASendHeldForTheBusStepIsSaidAtOnceAndNotAsLateness(t *testing.T) {
|
|
now := time.Date(2026, 10, 8, 18, 30, 0, 0, time.UTC)
|
|
b := aBusOnAnchor()
|
|
held := func(entered time.Duration) *signalFacts {
|
|
f := calm(now)
|
|
f.plans = []planFacts{{id: "plan-1", repository: "novox/mesh-host", commit: "c0ffee00", tier: 0, tiers: 1,
|
|
entered: now.Add(-entered), bound: 30 * time.Minute, waiting: refusedNote(b, 0), bus: true}}
|
|
f.bus = busFacts{module: "nats", to: b.to, from: b.from, machines: []string{"anchor"},
|
|
waits: []busWaitFacts{{plan: "plan-1", repository: "novox/mesh-host", commit: "c0ffee001122",
|
|
modules: []string{"mesh-host"}, since: now.Add(-time.Minute)}}}
|
|
return f
|
|
}
|
|
|
|
f := held(time.Minute)
|
|
got := watchBusWaits(f)
|
|
if len(got) != 1 {
|
|
t.Fatalf("a send held for the bus a minute ago raised %+v", got)
|
|
}
|
|
o := got[0]
|
|
if o.Key() != "bus.nats.step-waiting" || o.Kind != kindBusStepWaiting || o.Resolver != conditions.ResolverOperator {
|
|
t.Fatalf("raised %s (%s), resolver %q", o.Key(), o.Kind, o.Resolver)
|
|
}
|
|
for _, want := range []string{"anchor", "mesh-host", "88135ad0", "32307bd1", "mesh_call mesh-controller.bus",
|
|
`"upgrade": "true"`, `"reversible"`, `"irreversible"`, "2026-10-08T18:29:00Z", "plan-1"} {
|
|
if !strings.Contains(o.Summary, want) {
|
|
t.Errorf("its summary does not say %q: %s", want, o.Summary)
|
|
}
|
|
}
|
|
if !strings.Contains(o.Explanation, "mesh-host") || !strings.Contains(o.Headline, "bus upgrade") || o.Needs == "" {
|
|
t.Errorf("its words do not say what waits and what the operator does: %+v", o)
|
|
}
|
|
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
|
|
Resolved: o.Resolved, Needs: o.Needs}, "anchor"); !ok {
|
|
t.Errorf("its words are not plain: %s", why)
|
|
}
|
|
|
|
// Past S3's bound: still the bus's wait, never a stalled walk.
|
|
late := held(45 * time.Minute)
|
|
if s3 := watchPlans(late); len(s3) != 0 {
|
|
t.Fatalf("a walk held only by the bus step was said stalled: %+v", s3)
|
|
}
|
|
// A walk held for something else past its bound is still stalled.
|
|
other := held(45 * time.Minute)
|
|
other.plans[0].bus = false
|
|
if s3 := watchPlans(other); len(s3) != 1 {
|
|
t.Fatalf("a walk held for something else past its bound raised %+v", s3)
|
|
}
|
|
|
|
// Through the keeper: open at the first tick, cleared when the step started.
|
|
store := conditions.NewInMemory()
|
|
k := conditions.NewKeeper(t.Context(), conditions.Options{Store: store, History: store, Teller: &conditions.Told{},
|
|
Now: func() time.Time { return now }})
|
|
defer k.Close(context.Background())
|
|
w := &watchdogs{keeper: k, started: now.Add(-time.Hour)}
|
|
w.see(t.Context(), held(time.Minute))
|
|
open, err := k.Open(t.Context())
|
|
if err != nil || len(open) != 1 || open[0].Key != "bus.nats.step-waiting" {
|
|
t.Fatalf("after the first tick, open: %+v (%v)", open, err)
|
|
}
|
|
started := held(time.Minute)
|
|
started.bus = busFacts{module: "nats", to: b.to}
|
|
started.plans[0].bus = false
|
|
w.see(t.Context(), started)
|
|
if open, _ := k.Open(t.Context()); len(open) != 0 {
|
|
t.Fatalf("the step started, and open: %+v", open)
|
|
}
|
|
}
|