Replay issues 296, 299 and 300 as tests the commit before each fix fails (hq ADR 0237)
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 pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group replays/296-299-300 delivered: every member is delivered
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 pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group replays/296-299-300 delivered: every member is delivered
This commit is contained in:
@@ -6,13 +6,16 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// The replays of the controller's incidents (novox/hq to-be 45 §9, M9): each a scripted replay of what
|
||||
@@ -341,3 +344,181 @@ func TestReplaySilentWebAppIsRaisedWithinTwoLooks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **R296 — a plan's clock counts from its tier, not from its last save.** On 2026-10-07 a plan for a
|
||||
// merge to the catalogue, its one module's build asked at 19:48:41 and queued at the build seat for
|
||||
// minutes, read "tier 1 of 1, building for 1s", then 4s, then 9s: the line counted from the plan's last
|
||||
// save, and every advance — on the 30-second tick and after every build outcome on the mesh — saved it
|
||||
// whether or not anything moved. A clock reset by each save is never LATE, so `plans` and `status` could
|
||||
// not say a plan was stuck. The outcome asserted: a plan in its tier for forty minutes, saved seconds ago,
|
||||
// says forty minutes and LATE.
|
||||
func TestReplay296APlansLineCountsFromWhenItEnteredItsTier(t *testing.T) {
|
||||
now := time.Date(2026, 10, 7, 20, 30, 0, 0, time.UTC)
|
||||
entered := time.Date(2026, 10, 7, 19, 48, 41, 0, time.UTC)
|
||||
asked := entered
|
||||
p := inventory.Plan{ID: "plan-296", Repository: "novox/mesh-catalog", Branch: "main", Commit: "3ad22356",
|
||||
State: inventory.PlanBuilding, Created: entered, TierEntered: entered, Updated: now.Add(-4 * time.Second),
|
||||
Tiers: [][]string{{"app"}}, Modules: map[string]*inventory.PlanModule{"app": {State: "asked", AskedAt: &asked}}}
|
||||
line := planLine(p, now)
|
||||
if !strings.Contains(line, "for 41m19s") || !strings.Contains(line, "LATE") {
|
||||
t.Fatalf("a plan in its tier since 19:48:41, read at 20:30:00 and saved 4s before, reads %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// **R296, the save.** The same live case from the store's side: a plan whose only build was asked and
|
||||
// still waits is advanced as the tick and the mesh's build outcomes advance it, and is not written again —
|
||||
// its revision and its last save stay where its last change left them. Before the fix each advance saved
|
||||
// it, and its revision went from 3 to 6.
|
||||
func TestReplay296APlanStandingStillInItsTierIsNotSavedAgain(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
asked := asksRecorded(t)
|
||||
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: "1"},
|
||||
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", Ref: "main",
|
||||
BuiltFrom: "c0", Head: "c0"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "3ad22356aa",
|
||||
Paths: []string{"modules/app/index.ts"}, ModuleDirs: []string{"modules/app"}, ModuleDirsSaid: true}
|
||||
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
advanceHeld(ctx, open)
|
||||
recent, err := inv.RecentPlans(ctx, 1)
|
||||
if err != nil || len(recent) != 1 || len(*asked) != 1 {
|
||||
t.Fatalf("no plan waiting on its build: %v %v, asked %v", recent, err, *asked)
|
||||
}
|
||||
before := recent[0]
|
||||
if before.State != inventory.PlanBuilding || before.Modules["app"] == nil || before.Modules["app"].State != "asked" {
|
||||
t.Fatalf("the plan is not waiting on its build: %+v", before)
|
||||
}
|
||||
for range 3 {
|
||||
advanceHeld(ctx, open)
|
||||
}
|
||||
after, err := inv.PlanByID(ctx, before.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Revision != before.Revision || !after.Updated.Equal(before.Updated) {
|
||||
t.Fatalf("a plan that did not move was saved again: revision %d → %d, saved %s → %s",
|
||||
before.Revision, after.Revision, before.Updated, after.Updated)
|
||||
}
|
||||
}
|
||||
|
||||
// **R299 — a holder of a seat whose verbs are all optional is not silent.** On 2026-10-07 the
|
||||
// node-uplink seat gained its first verbs, `resolvers` and `links`, both optional while its holders catch
|
||||
// up (ADR 0246, step 1); its holders served neither, so they registered nothing on the bus's discovery for
|
||||
// it, and the self-check's D3 raised `seat.node-uplink.<machine>.silent` on every machine of the mesh,
|
||||
// which the healer then acted on. The outcome asserted: through D3 itself — the seats read back from the
|
||||
// store as the controller reads them, a holder on record on every machine heard from, and nothing
|
||||
// answering the discovery — no holder of a seat whose verbs are all optional is said silent.
|
||||
//
|
||||
// The live seat is replayed while its verbs are all optional; beside it, a seat whose verbs are all
|
||||
// optional by definition, so the outcome is still held once node-uplink's verbs are required (step 3).
|
||||
func TestReplay299AHolderOfASeatWhoseVerbsAreAllOptionalIsNotSilent(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
catchingUp := catalogue.Seat{Name: "node-catching-up", Scope: catalogue.ScopeNode, Decision: "novox/hq issue 299",
|
||||
Serves: []catalogue.Verb{{Name: "first", Optional: true}, {Name: "second", Optional: true}}}
|
||||
if _, err := inv.SeedSeats(ctx, append(catalogue.DefaultSeats(), catchingUp)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err := inv.Seats(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The working set as the serving controller holds it: the store's rows, the compiled seats' marks.
|
||||
// A seat the compiled set does not know keeps its mark only as defined here.
|
||||
for i := range rows {
|
||||
if rows[i].Name == catchingUp.Name {
|
||||
rows[i].Serves = catchingUp.Serves
|
||||
}
|
||||
}
|
||||
catalogue.UseSeats(rows)
|
||||
t.Cleanup(func() { catalogue.UseSeats(catalogue.DefaultSeats()) })
|
||||
|
||||
seats := []string{catchingUp.Name}
|
||||
if uplink, ok := catalogue.SeatNamed("node-uplink"); ok && len(uplink.Serves) > 0 {
|
||||
allOptional := true
|
||||
for _, v := range uplink.Serves {
|
||||
allOptional = allOptional && v.Optional
|
||||
}
|
||||
if allOptional {
|
||||
seats = append(seats, "node-uplink")
|
||||
} else {
|
||||
t.Logf("node-uplink's verbs are required at this commit; the live seat is not replayed, its kind is")
|
||||
}
|
||||
}
|
||||
|
||||
register(t, open, catalogue.Manifest{Module: "networkmanager", Version: "1"})
|
||||
now := time.Now()
|
||||
var heard []machineFacts
|
||||
for _, node := range []string{"anchor", "laptop"} {
|
||||
if _, err := inv.Assign(ctx, node, "networkmanager"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, seat := range seats {
|
||||
if err := inv.HoldSeat(ctx, seat, catalogue.ScopeNode, node, "networkmanager"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
heard = append(heard, machineFacts{name: node, lastHeard: now})
|
||||
}
|
||||
js, err := broker.Dial(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(js.Close)
|
||||
d := &doctor{open: open, js: js, watchdogs: &watchdogs{last: &signalFacts{now: now, machines: heard}}}
|
||||
|
||||
found, err := probeHolders(ctx, d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, o := range found {
|
||||
for _, seat := range seats {
|
||||
if strings.HasPrefix(o.ID, seat+".") {
|
||||
t.Errorf("a holder serving none of a seat's optional verbs was said silent: %s (%s)", o.Summary, o.Said)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **R300 — a merge that adds a module builds it.** On 2026-10-07 a pull request to the catalogue added
|
||||
// one module, systemd-resolved; its delivery plan said "builds new: modules/systemd-resolved, sent
|
||||
// nowhere", and at the merge the controller said it "changed nothing any module the mesh holds is built
|
||||
// from" and built nothing, so the module was never registered and `assign` refused it until a person
|
||||
// built it by hand. The outcome asserted: the merge asks the build seat for the new module's directory, at
|
||||
// the branch merged into, from the repository as the mesh spells it, and opens no plan for it.
|
||||
func TestReplay300AMergeAddingAModuleAsksForItsBuild(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
inv := open.inventory
|
||||
var asked [][3]string
|
||||
was := askABuild
|
||||
askABuild = func(_ context.Context, source buildSource, path, ref string) (string, error) {
|
||||
asked = append(asked, [3]string{source.Repository, path, ref})
|
||||
return "build-" + path, nil
|
||||
}
|
||||
t.Cleanup(func() { askABuild = was })
|
||||
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "networkmanager", Version: "1"},
|
||||
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/networkmanager", Ref: "main",
|
||||
BuiltFrom: "c0", Head: "c0"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "3da80a4b00",
|
||||
Paths: []string{"modules/systemd-resolved/module.json", "modules/systemd-resolved/cmd/main.go"},
|
||||
ModuleDirs: []string{"modules/systemd-resolved"}, ModuleDirsSaid: true}
|
||||
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := [3]string{"novox/mesh-catalog", "modules/systemd-resolved", "main"}
|
||||
if len(asked) != 1 || asked[0] != want {
|
||||
t.Fatalf("the merge adding modules/systemd-resolved asked the build seat for %v, not %v", asked, want)
|
||||
}
|
||||
if plans, err := inv.OpenPlans(ctx); err != nil || len(plans) != 0 {
|
||||
t.Fatalf("a merge moving nothing the mesh holds opened a plan: %+v %v", plans, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user