Files
mesh-controller/cmd/mesh-controller/replays_integration_test.go
T

502 lines
18 KiB
Go

//go:build integration
package main
import (
"context"
"encoding/json"
"flag"
"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"
)
func TestReplay263AnIdentityTooLongNeverRefusesItsProvidersMachine(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "resolver", Version: "1",
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}}})
register(t, open, catalogue.Manifest{Module: "networkmanager", Version: "1",
Requires: []string{"wildcard-resolution"}})
for _, a := range [][2]string{{"anchor", "resolver"}, {"laptop", "networkmanager"}} {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
}
}
// The consumer's machine composes first, as a push does: what it is sent is what its provider grants.
for _, node := range []string{"laptop", "anchor"} {
plan, settings, err := planFor(ctx, open, node)
if err != nil {
t.Fatalf("%s does not resolve: %v", node, err)
}
if _, err := declarationFor(ctx, open, node, plan, settings); err != nil {
t.Fatalf("%s cannot be sent anything — the consumer networkmanager on laptop, identified "+
"mesh_laptop_networkmanager (26 characters), refused it: %v", node, err)
}
}
}
func TestReplay273AConsumerBesideItsStoreStaysBoundToIt(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
}
for _, m := range []catalogue.Manifest{
{Module: "store", Version: "1",
Provides: []catalogue.Offer{{Name: "postgres-database", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-store", Scope: catalogue.ScopeMesh}},
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
Grants: map[string]string{"postgres-database": "/var/lib/mesh/store/grants"}},
{Module: "resolver", Version: "1",
Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh}}},
{Module: "network", Version: "1", Requires: []string{"wildcard-resolution"}},
{Module: "board", Version: "1", Requires: []string{"postgres-database"}},
} {
register(t, open, m)
}
assignAll := func(pairs ...[2]string) {
for _, a := range pairs {
if _, err := assign(ctx, open, a[0], a[1]); err != nil {
t.Fatalf("assign %s %s: %v", a[0], a[1], err)
}
}
}
// The control node holds the mesh's store and resolver seats; the home server runs its own of each.
assignAll([2]string{"anchor", "store"}, [2]string{"anchor", "resolver"})
for _, seat := range [][2]string{{"mesh-store", "store"}, {"mesh-dns-resolver", "resolver"}} {
if err := inv.HoldSeat(ctx, seat[0], catalogue.ScopeMesh, "anchor", seat[1]); err != nil {
t.Fatal(err)
}
}
assignAll([2]string{"laptop", "store"}, [2]string{"laptop", "resolver"}, [2]string{"laptop", "network"},
[2]string{"laptop", "board"})
plan, _, err := planFor(ctx, open, "laptop")
if err != nil {
t.Fatal(err)
}
var board, network string
for _, n := range plan.Needs {
switch {
case n.For == "board" && n.Name == "postgres-database":
board = n.From
case n.For == "network" && n.Name == "wildcard-resolution":
network = n.From
}
}
if board != "laptop" {
t.Fatalf("the consumer beside its store was bound to the store on %q, which would make it a new, empty "+
"database there (issue 273)", board)
}
if network != "anchor" {
t.Fatalf("the resolver was bound to %q; its seat is held on anchor (issue 258)", network)
}
}
func TestReplayCrashLoopFailsItsGateOnTheFirstMachine(t *testing.T) {
withTheGate(t) // the first-machine gate's own path (novox/hq ADR 0360 keeps it for earlier walks)
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
crashLoop := []byte(`{"contract":1,"at":"2026-10-07T00:00:00Z","resources":[{"module":"app","resource":"app.server",` +
`"kind":"container","target":"app-server","state":"unhealthy","reason":"restarting","since":"2026-10-07T00:00:00Z",` +
`"streak":5,"restarts":2}]}`)
if path := os.Getenv("MESH_REPLAY_STATEMENT"); path != "" {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("the engine's statement of the crash loop: %v", err)
}
crashLoop = raw
}
// `null` is an engine that states nothing — older than the judging — and its reports carry no health.
var stated map[string]any
if err := json.Unmarshal(crashLoop, &stated); err != nil {
t.Fatal(err)
}
for _, b := range []inventory.Build{
{ID: "build-1", Module: "app", Commit: "c1", Repository: "novox/mesh-catalog", Path: "modules/app",
Asked: time.Now().Add(-2 * time.Hour), At: time.Now().Add(-2 * time.Hour)},
{ID: "build-2", Module: "app", Commit: "c2", Repository: "novox/mesh-catalog", Path: "modules/app",
Asked: time.Now().Add(-time.Minute), At: time.Now().Add(-time.Minute)},
} {
manifest, _ := json.Marshal(catalogue.Manifest{Module: "app", Version: b.Commit})
b.Manifest = manifest
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
registerAt := func(commit string, asked time.Time) {
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "app", Version: commit},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/app", BuiltFrom: commit,
Head: commit, Asked: asked}); err != nil {
t.Fatal(err)
}
}
registerAt("c1", time.Now().Add(-2*time.Hour))
for _, n := range []string{"anchor", "laptop"} {
if _, err := inv.Assign(ctx, n, "app"); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, nodeID(t, open, n), "d-"+n+"-c1", map[string]string{"app": "c1"}); err != nil {
t.Fatal(err)
}
}
registerAt("c2", time.Now().Add(-time.Minute))
// The machine: what it is sent is applied, and its report says what runs is starting.
listener := nudgingListener{Enrolment: link.Enrolment{Inventory: inv}}
sequence := int64(0)
say := func(node, digest, state string) {
t.Helper()
sequence++
health := map[string]any{}
for k, v := range stated {
health[k] = v
}
health["at"] = time.Now().UTC().Format(time.RFC3339Nano)
if state != "" && stated != nil {
resources := []any{}
for _, r := range stated["resources"].([]any) {
kept := map[string]any{}
for k, v := range r.(map[string]any) {
kept[k] = v
}
kept["state"], kept["reason"] = state, ""
resources = append(resources, kept)
}
health["resources"] = resources
}
said := map[string]any{"node": node, "applied": []string{"app.server"}, "declared": digest,
"report_sequence": sequence}
if stated != nil {
said["health"] = health
}
body, _ := json.Marshal(said)
var report link.Report
if err := json.Unmarshal(body, &report); err != nil {
t.Fatal(err)
}
if _, err := listener.Heard(ctx, report); err != nil {
t.Fatal(err)
}
}
var sent [][]string
last := map[string]string{}
n := 0
wasSend := sendRollout
sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) {
sent = append(sent, append([]string(nil), names...))
current, err := open.inventory.CurrentBuilds(ctx)
if err != nil {
return nil, err
}
for _, node := range names {
n++
digest := fmt.Sprintf("d-%s-%d", node, n)
if err := open.inventory.RecordSent(ctx, nodeID(t, open, node), digest, map[string]string{"app": current["app"].Commit}); err != nil {
return nil, err
}
last[node] = digest
say(node, digest, "starting")
}
return names, nil
}
t.Cleanup(func() { sendRollout = wasSend })
wasWithin, wasEvery, wasBound := gateWithin, gateEvery, gateBound
// The witness window is the node-engine's, from a start no test makes (novox/hq ADR 0351).
wasWindow := gateWitnessWindow
gateWitnessWindow = 0
t.Cleanup(func() { gateWitnessWindow = wasWindow })
gateWithin, gateEvery = time.Hour, 0
t.Cleanup(func() { gateWithin, gateEvery, gateBound = wasWithin, wasEvery, wasBound })
built := time.Now().UTC()
plan := inventory.Plan{ID: "plan-crashloop", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2",
Created: built, State: inventory.PlanBuilding, Tiers: [][]string{{"app"}},
Modules: map[string]*inventory.PlanModule{"app": {State: "built", BuiltAt: &built, Commit: "c2", Build: "build-2"}}}
if err := inv.SavePlan(ctx, &plan); err != nil {
t.Fatal(err)
}
advancePlans(ctx, open) // the first machine is sent the new build, and starts it
if len(sent) == 0 || !slices.Equal(sent[0], []string{"anchor"}) {
t.Fatalf("sent %v, not the first machine first", sent)
}
advancePlans(ctx, open) // judged while it starts
// Its container exits at start, and the runtime restarts it: the machine says so, again and again.
for i := 0; i < 6 && len(sent) == 1; i++ {
say("anchor", last["anchor"], "")
advancePlans(ctx, open)
}
gateBound = -time.Second // and the bound passes
advancePlans(ctx, open)
p, err := inv.PlanByID(ctx, "plan-crashloop")
if err != nil {
t.Fatal(err)
}
gate := p.Modules["app"].Gate
for _, names := range sent {
if slices.Contains(names, "laptop") {
t.Fatalf("the crash loop passed its gate on anchor and was sent to laptop: sent %v, the gate %+v", sent, gate)
}
}
if gate == nil || gate.Verdict != inventory.GateFailed || p.State != inventory.PlanFailed {
t.Fatalf("a crash-looping build did not fail its gate on the first machine: the plan is %s (%s), its gate %+v",
p.State, p.Note, gate)
}
if current, _ := inv.CurrentBuilds(ctx); current["app"].Commit != "c1" {
t.Fatalf("the module is registered at %s, not put back to c1", current["app"].Commit)
}
}
func TestReplaySilentWebAppIsRaisedWithinTwoLooks(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
silent := []byte(`{"contract":2,"at":"2026-10-07T00:00:00Z","resources":[{"module":"app","resource":"app.server",` +
`"kind":"container","target":"app-server","state":"unhealthy","reason":"http / on web: no answer within 5s",` +
`"since":"2026-10-07T00:00:00Z","streak":3,"check":"http"}]}`)
if path := os.Getenv("MESH_REPLAY_STATEMENT"); path != "" {
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("the engine's statement of the silent web application: %v", err)
}
silent = raw
}
var h link.Health
if err := json.Unmarshal(silent, &h); err != nil {
t.Fatal(err)
}
if h.Contract == 0 {
t.Fatal("the engine states nothing of the silent web application: it is older than the judging")
}
open1 := func() []conditions.Condition {
t.Helper()
list, err := conditionsFrom.Open(ctx)
if err != nil {
t.Fatal(err)
}
return list
}
for look := 1; look <= 2; look++ {
at := time.Now().Add(time.Duration(look) * time.Second)
h.At = at
if err := stateHealth(ctx, open.inventory, conditionsFrom, "anchor", h, at); err != nil {
t.Fatal(err)
}
raised := open1()
switch {
case look == 1 && len(raised) != 0:
t.Fatalf("one look raised %v", raised[0].Key)
case look == 2 && (len(raised) != 1 || raised[0].Key != "module.app.anchor.unhealthy"):
t.Fatalf("two looks in a row did not raise the module's condition: %v", raised)
case look == 2:
t.Logf("raised on the second look: %s", raised[0].Summary)
}
}
}
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)
}
}
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)
}
}
}
}
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)
}
}
func TestReplay301APersonsPushOfAHeldRecordedBuildIsNoRepair(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
image := "registry.invalid:5000/resolver/server@sha256:" + strings.Repeat("e", 64)
if _, _, err := takeIn(ctx, inv, asTheOperator(t, inv, aContainerBuild(t, "resolver", "c1111111", catalogue.PolicyRecord, start,
map[string][2]string{"server": {image, ""}}))); err != nil {
t.Fatal(err)
}
for _, node := range []string{"anchor", "laptop"} {
if _, err := inv.Assign(ctx, node, "resolver"); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, nodeID(t, open, node), "d-"+node, map[string]string{"resolver": "c1111111"}); err != nil {
t.Fatal(err)
}
}
// The merge: the resolver's new build, registered and held for a person's word.
if _, _, err := takeIn(ctx, inv, aContainerBuild(t, "resolver", "c2222222", catalogue.PolicyRecord,
start.Add(time.Minute), map[string][2]string{"server": {image, "runtime"}})); err != nil {
t.Fatal(err)
}
conn := onATestBus(t)
// The operator's yes, machine by machine, as the seat's push records it before it sends.
for _, node := range []string{"anchor", "laptop"} {
set := flag.NewFlagSet("push", flag.ContinueOnError)
why := addHandActFlags(set)
if err := set.Parse([]string{"--why", "operator's yes: the resolver's new build on " + node,
"--cause", "split-dns"}); err != nil {
t.Fatal(err)
}
why.record(ctx, "push", []string{node})
}
acts, err := link.HandActs(ctx, conn, start)
if err != nil {
t.Fatal(err)
}
if len(acts) != 2 {
t.Fatalf("the two pushes were recorded as %d act(s): %+v", len(acts), acts)
}
f := calm(time.Now())
f.handActs = acts
for _, o := range watchHandActs(f) {
if strings.Contains(o.ID, "split-dns") {
t.Fatalf("a person's push of a held recorded build was counted as a repair: %s — %s", o.ID, o.Summary)
}
}
}