package main import ( "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) // novox/hq ADR 0419: a group's order, from the module graph and the manifests' change alone, the same on every // reading — what a module stands on before it, the controller before a manifest that needs it, the node-engine // before the controller, a verb's server before its caller — and no `after:` line read. func TestAGroupIsOrderedByTheGraphAndTheManifestsChange(t *testing.T) { members := []orderMember{ {ID: "cat", Repository: "novox/mesh-catalog"}, {ID: "ctl", Repository: "novox/mesh-controller"}, {ID: "host", Repository: "novox/mesh-host"}, {ID: "base", Repository: "novox/base"}, {ID: "app", Repository: "novox/app"}, {ID: "lab", Repository: "novox/lab"}, } reach := map[string]orderReach{ "cat": {Moved: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}}, "ctl": {Moved: []string{"mesh-controller"}, Manifests: []string{"module.json"}}, "host": {Moved: []string{"mesh-host"}, Manifests: []string{"module.json"}}, "base": {Moved: []string{"base"}}, "app": {Moved: []string{"app"}}, "lab": {Moved: []string{"lab"}}, } edges := []inventory.Edge{{From: "app", To: "base", Kind: inventory.EdgeStandsOn}, {From: "gitea", To: "postgres", Kind: inventory.EdgeDeclared}} changes := map[string]moduleChange{"gitea": {Manifest: true}, "mesh-controller": {Manifest: true}, "mesh-host": {Manifest: true}, "lab": {Serves: []string{"seat.v"}}, "app": {Invokes: []string{"seat.v"}}} got := orderOf(members, reach, edges, changes) if len(got.Cycle) > 0 { t.Fatalf("a cycle where there is none: %v", got.Cycle) } // By repository among those with nothing before them: app waits for base and lab; gitea's catalogue for the controller. want := []string{"base", "lab", "app", "host", "ctl", "cat"} if !reflect.DeepEqual(got.Order, want) { t.Fatalf("ordered %v, wanted %v; pairs %+v", got.Order, want, got.Pairs) } why := map[string]string{} for _, p := range got.Pairs { why[p.Before+">"+p.After] = p.Why } for pair, reason := range map[string]string{"host>ctl": ruleEngineFirst, "ctl>cat": ruleVersionSkew, "ctl>host": "", "base>app": orderBuiltBy, "lab>app": ruleVerbBeforeCaller + ": seat.v"} { if why[pair] != reason { t.Errorf("%s is ordered %q, wanted %q", pair, why[pair], reason) } } // The same members in another order read the same. shuffled := []orderMember{members[5], members[3], members[0], members[4], members[2], members[1]} if again := orderOf(shuffled, reach, edges, changes); !reflect.DeepEqual(again.Order, want) { t.Fatalf("another reading ordered %v", again.Order) } } // A cycle is the one refusal, and it names every module and edge. A description's `after:` line changes nothing. func TestAGroupInACycleIsNamedAndNothingElseIsRefused(t *testing.T) { members := []orderMember{ {ID: "x", Repository: "novox/x", Number: 7}, {ID: "y", Repository: "novox/y", Number: 9}, } reach := map[string]orderReach{"x": {Moved: []string{"mx"}}, "y": {Moved: []string{"my"}}} changes := map[string]moduleChange{ "mx": {Serves: []string{"s.a"}, Invokes: []string{"s.b"}}, "my": {Serves: []string{"s.b"}, Invokes: []string{"s.a"}}, } got := orderOf(members, reach, nil, changes) if !reflect.DeepEqual(got.Cycle, []string{"x", "y"}) || len(got.Order) > 0 { t.Fatalf("a cycle was ordered: %v, cycle %v", got.Order, got.Cycle) } for _, w := range []string{"mx", "my", "s.a", "s.b"} { if !strings.Contains(got.CycleSaid, w) { t.Errorf("the cycle does not say %q: %s", w, got.CycleSaid) } } // Without the edges the same members are ordered by repository. got = orderOf(members, reach, nil, nil) if !reflect.DeepEqual(got.Order, []string{"x", "y"}) || len(got.Cycle) > 0 || len(got.Pairs) > 0 { t.Fatalf("members with no edge: %v %v %+v", got.Order, got.Cycle, got.Pairs) } } // novox/hq ADR 0239 decision 8: a walk waits for the delivery's owner only while the seat has a holder on // record, and never one that moves a module on the controller's own path. func TestAWalkWaitsOnlyWhileTheDeliverySeatIsHeldAndNeverForTheCore(t *testing.T) { holder := inventory.Entry{Manifest: catalogue.Manifest{Module: "mesh-delivery", Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}}, On: []string{"anchor"}} unassigned := holder unassigned.On = nil for _, c := range []struct { entries []inventory.Entry moved []string waits bool }{ {nil, []string{"gitea"}, false}, {[]inventory.Entry{unassigned}, []string{"gitea"}, false}, {[]inventory.Entry{holder}, []string{"gitea", "plex"}, true}, {[]inventory.Entry{holder}, []string{"gitea", "mesh-controller"}, false}, {[]inventory.Entry{holder}, []string{"mesh-host"}, false}, {[]inventory.Entry{holder}, []string{"node-tools"}, false}, {[]inventory.Entry{holder}, []string{"nats"}, false}, {[]inventory.Entry{holder}, []string{"mesh-delivery", "gitea"}, false}, } { d := awaitsFor(c.entries, c.moved) if (d != nil) != c.waits { t.Errorf("held %v, moving %v: waits %v, wanted %v", len(c.entries) > 0 && len(c.entries[0].On) > 0, c.moved, d != nil, c.waits) } if d != nil && d.Awaits != catalogue.DeliverySeat { t.Errorf("waits for %q", d.Awaits) } } } // novox/hq ADR 0239: with mesh-delivery on record, a merge opens its walk and asks nothing until the // delivery's word; the word starts it; the core's own merge never waits; a person starts a waiting walk by // hand when the owner is down, and the owner's stop ends one as stopped. // The verbs mesh-delivery asks with become the commands they name, and nothing a caller sends is passed over. func TestTheDeliveryVerbsComposeTheirCommands(t *testing.T) { for _, c := range []struct { verb string args map[string]any want []string }{ {"deliver", map[string]any{"plan": "plan-1", "why": "its turn"}, []string{"delivery", "go", "plan-1", "--by", "mesh-delivery", "--why", "its turn"}}, {"delivery-stop", map[string]any{"plan": "plan-1", "why": "w", "by": "jochen"}, []string{"delivery", "stop", "plan-1", "--why", "w", "--by", "mesh-delivery for jochen"}}, {"delivery-walks", map[string]any{}, []string{"delivery", "walks"}}, {"delivery-walks", map[string]any{"plan": "plan-1"}, []string{"delivery", "walks", "--plan", "plan-1"}}, {"delivery-order", map[string]any{"members": "[]"}, []string{"delivery", "order", "--members", "[]"}}, {"delivery-check", map[string]any{"group": "feat/x", "members": "[]"}, []string{"delivery", "check", "--group", "feat/x", "--members", "[]"}}, {"delivery-plan", map[string]any{"repository": "novox/a", "paths": "x", "head": "c0"}, []string{"delivery", "plan", "--repository", "novox/a", "--paths", "x", "--head", "c0"}}, {"plans", map[string]any{"go": "plan-1", "why": "down"}, []string{"plans", "go", "plan-1", "--why", "down"}}, } { got, err := argvFor(c.verb, c.args) if err != nil || !reflect.DeepEqual(got, c.want) { t.Errorf("%s %v → %v %v, wanted %v", c.verb, c.args, got, err, c.want) } } if _, err := argvFor("deliver", map[string]any{}); err == nil { t.Error("deliver without a walk was composed") } if _, err := argvFor("delivery-stop", map[string]any{"plan": "p"}); err == nil { t.Error("a stop without why was composed") } } // A verb runs as a command of its own: what it keeps of a walk is still said as plan-moved, on a bus of the // command's own, so the delivery's owner hears it at once and not only when it reads the walks back.