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:
@@ -6,13 +6,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/broker"
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
"github.com/novox/mesh-controller/internal/conditions"
|
"github.com/novox/mesh-controller/internal/conditions"
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
"github.com/novox/mesh-controller/internal/link"
|
"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
|
// 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