Merge pull request 'Replay issues 296, 299 and 300 as tests the commit before each fix fails (hq ADR 0237)' (#124) from replays/296-299-300 into main

This commit was merged in pull request #124.
This commit is contained in:
2026-10-07 21:55:17 +00:00
+181
View File
@@ -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)
}
}