package assign import ( "slices" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) func meshOf() Mesh { return Mesh{ Manifests: map[string]catalogue.Manifest{ "mail": {Module: "mail", Version: "1", Requires: []string{"database"}}, "store": {Module: "store", Version: "1", Provides: []catalogue.Offer{{Name: "database"}}}, "web": {Module: "web", Version: "1"}, }, Assigned: map[string][]string{"ace": {"mail", "store"}, "g14": {"web"}}, } } func hasWarning(v delivery.CompositionVerdict, part string) bool { return slices.ContainsFunc(v.Warnings, func(w string) bool { return strings.Contains(w, part) }) } // 1297/AC1 (its unit half): a change touching one module on one machine composes that machine alone, passes, names // the machine and the mesh-sdk it composed with, and says what it did not judge. func TestAChangeOfOneModuleComposesItsMachine(t *testing.T) { c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"web": {Module: "web", Version: "2"}}} v := Compose(meshOf(), c, "v0.1.14-x", 0) if v.State != delivery.CompositionPass || !slices.Equal(v.Machines, []string{"g14"}) || v.SDK != "v0.1.14-x" { t.Fatalf("composition = %+v, want a pass on g14 alone", v) } if !hasWarning(v, "issue 1461") { t.Errorf("the verdict does not say what it did not judge: %v", v.Warnings) } if _, err := delivery.EncodeCompositionVerdict(v); err != nil { t.Errorf("the verdict cannot be written: %v", err) } } // 1297/AC2: a change that removes a module a machine runs fails, naming the machine and the module. func TestRemovingAModuleAMachineRunsFails(t *testing.T) { c := Change{Group: "g", Heads: "0123456789ab", Removed: []string{"web"}} v := Compose(meshOf(), c, "sdk", 0) if v.State != delivery.CompositionFail || v.Machine != "g14" || !strings.Contains(v.Words, "g14 runs web") { t.Fatalf("composition = %+v, want a fail naming g14 and web", v) } } // 1297/B3c: a machine that composes on the trunk and not with the change fails it, naming the machine and why; the // consumer's machine is touched when its provider changes. func TestAMachineTheChangeBreaksFails(t *testing.T) { c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"store": {Module: "store", Version: "2"}}} v := Compose(meshOf(), c, "sdk", 0) if v.State != delivery.CompositionFail || v.Machine != "ace" || !strings.Contains(v.Words, "database") { t.Fatalf("composition = %+v, want a fail on ace naming the provision nothing provides", v) } // ADR 0559 §5 (issue 1126): a change of placement composes its placement laid over the trunk's input and fails naming // the node it breaks: a module no manifest names placed on g14; ace, // is no longer composed with it; a placement whose input cannot be read fails; a placement that breaks nothing passes, g14 touched. m := meshOf() placed := func(assigned map[string][]string) *ChangePlacement { in := input(m.Manifests, assigned) return &ChangePlacement{Placements: map[string]*delivery.Placement{"g14": {Node: "g14"}}, Input: &in} } for _, row := range []struct { name string cp *ChangePlacement state string machine string words string }{ {"a placement that breaks g14", placed(map[string][]string{"ace": {"mail", "store"}, "g14": {"web", "ghost"}}), delivery.CompositionFail, "g14", "ghost"}, {"a placement whose input is not read", &ChangePlacement{Placements: map[string]*delivery.Placement{"g14": nil}, Unread: "no record of g14"}, delivery.CompositionFail, "", "no record of g14"}, {"a placement that breaks nothing", placed(map[string][]string{"ace": {"mail", "store"}, "g14": {"web"}}), delivery.CompositionPass, "", ""}, } { v := Compose(m, Change{Group: "g", Heads: "0123456789ab", Placement: row.cp}, "sdk", 0) if v.State != row.state || v.Machine != row.machine || !strings.Contains(v.Words, row.words) || !slices.Contains(v.Machines, "g14") { t.Errorf("%s: %+v", row.name, v) } } // ADR 0559 §5: the placed nodes' answers. ace is placed on the trunk and its candidate moves web there (intended); // g14 is not placed on the trunk (the seed), so nothing is intended there; novox is placed and unchanged. at := time.Date(2026, 10, 27, 2, 0, 0, 0, time.UTC) cp := &ChangePlacement{Head: "h2", Written: at, Revisions: map[string]uint64{"ace": 7, "g14": 8}, Placements: map[string]*delivery.Placement{"ace": {Node: "ace"}, "g14": {Node: "g14"}}} intended := map[string]map[string]bool{"ace": {"web": true}} same := func(rev uint64) *delivery.CandidateAnswer { return &delivery.CandidateAnswer{Head: "h2", Revision: rev, Verdict: delivery.CandidateSame, Same: []string{"mail"}} } placedNodes := []string{"ace", "g14", "novox"} for _, row := range []struct { name string answers map[string]*delivery.CandidateAnswer now time.Time state string words string }{ {"all the same", map[string]*delivery.CandidateAnswer{"ace": same(7), "g14": same(8), "novox": same(0)}, at, delivery.CompositionPass, ""}, {"a module added where intended", map[string]*delivery.CandidateAnswer{"ace": {Head: "h2", Revision: 7, Verdict: delivery.CandidateDiffers, Added: []delivery.ModuleAdded{{Module: "web", Unissued: []string{"its own secret"}}}}, "g14": same(8), "novox": same(0)}, at, delivery.CompositionPass, ""}, {"a module differing where not intended", map[string]*delivery.CandidateAnswer{"ace": {Head: "h2", Revision: 7, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{Module: "mail", Why: "the port differs"}}}, "g14": same(8), "novox": same(0)}, at, delivery.CompositionFail, "ace composes mail differently"}, {"a node's first placement file with a module differing", map[string]*delivery.CandidateAnswer{"ace": same(7), "g14": {Head: "h2", Revision: 8, Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{Module: "web", Why: "a binding differs"}}}, "novox": same(0)}, at, delivery.CompositionFail, "g14 composes web differently"}, {"a refusal", map[string]*delivery.CandidateAnswer{"ace": same(7), "g14": {Head: "h2", Revision: 8, Verdict: delivery.CandidateRefused, Refusals: []delivery.ModuleRefused{{Module: "web", Why: "no own secret"}}}, "novox": same(0)}, at, delivery.CompositionFail, "g14 refuses"}, {"a node not answering, within its bound", map[string]*delivery.CandidateAnswer{"ace": same(7), "novox": same(0)}, at.Add(AnswerBound - time.Second), delivery.CompositionWaits, "g14 (offline)"}, {"an answer of an earlier revision, past its bound", map[string]*delivery.CandidateAnswer{"ace": same(6), "g14": same(8), "novox": same(0)}, at.Add(AnswerBound), delivery.CompositionFail, "ace " + delivery.CompositionUnanswered}, {"its later answer", map[string]*delivery.CandidateAnswer{"ace": same(7), "g14": same(8), "novox": same(0)}, at.Add(time.Hour), delivery.CompositionPass, ""}, } { pass := delivery.CompositionVerdict{Group: "g", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "sdk", Machines: []string{"ace", "g14"}} v := WithAnswers(pass, JudgeAnswers(cp, placedNodes, row.answers, intended, map[string]bool{"ace": true, "novox": true}, row.now)) if v.State != row.state || !strings.Contains(v.Words, row.words) { t.Errorf("%s: %+v", row.name, v) } if _, err := delivery.EncodeCompositionVerdict(v); err != nil { t.Errorf("%s: the verdict cannot be written: %v", row.name, err) } } trunk := map[string]delivery.Placement{"ace": {Node: "ace", Modules: []delivery.PlacedModule{{Module: "mail"}}}, "novox": {Node: "novox"}} if got := PlacedNodes(trunk, &ChangePlacement{Placements: map[string]*delivery.Placement{"novox": nil, "g14": {Node: "g14"}}}); !slices.Equal(got, []string{"ace", "g14", "novox"}) { t.Errorf("placed at the head or on the trunk = %v", got) } got, _ := Intended(nil, trunk, &ChangePlacement{Placements: map[string]*delivery.Placement{"ace": {Node: "ace", Modules: []delivery.PlacedModule{{Module: "mail", Ports: map[string]int{"25": 2525}}, {Module: "web"}}}, "g14": {Node: "g14", Modules: []delivery.PlacedModule{{Module: "web"}}}}}, nil) if !got["ace"]["mail"] || !got["ace"]["web"] || len(got["g14"]) != 0 { t.Errorf("intended = %v, want ace's mail and web, nothing on g14 the trunk does not place", got) } } // 1297/B3c: what was already broken on the trunk is said and fails nothing. func TestWhatWasBrokenBeforeFailsNothing(t *testing.T) { m := meshOf() m.Manifests["store"] = catalogue.Manifest{Module: "store", Version: "1"} c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"mail": {Module: "mail", Version: "2", Requires: []string{"database"}}}} v := Compose(m, c, "sdk", 0) if v.State != delivery.CompositionPass || !hasWarning(v, "on the trunk either") { t.Fatalf("composition = %+v, want a pass saying ace was already broken", v) } } // 1297/B3c: an unreadable manifest and a module defined twice fail; more than twelve rebuilt modules warn. func TestUnreadableTwiceAndWide(t *testing.T) { c := Change{Group: "g", Heads: "0123456789ab", Unreadable: map[string]string{"web": "unknown field \"scope\""}, Rebuilt: 13} v := Compose(meshOf(), c, "sdk", 0) if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "web cannot be read") || !hasWarning(v, "rebuilds 13 modules") { t.Fatalf("composition = %+v", v) } c = Change{Group: "g", Heads: "0123456789ab", Twice: map[string][]string{"web": {"novox/a", "novox/b"}}, Rebuilt: 12} v = Compose(meshOf(), c, "sdk", 0) if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "web is defined twice") || hasWarning(v, "rebuilds") { t.Fatalf("composition = %+v", v) } } // 1622's criterion as R4g delivers it: a group with a mesh-sdk member is composed with the assigner's own mesh-sdk, and // the verdict says so, naming the member's head it did not use; a group without one says nothing of it. func TestAGroupWithAnSDKMemberSaysWhichSDKComposedIt(t *testing.T) { c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"web": {Module: "web", Version: "2"}}, SDKHead: "f625d98713e4"} v := Compose(meshOf(), c, "v0.1.14-own", 0) if !hasWarning(v, "not the group's mesh-sdk head f625d98713e4") || !hasWarning(v, "v0.1.14-own") || !hasWarning(v, "1623") { t.Errorf("the verdict does not say it composed with its own mesh-sdk: %v", v.Warnings) } c.SDKHead = "" if v := Compose(meshOf(), c, "v0.1.14-own", 0); hasWarning(v, "mesh-sdk head") { t.Errorf("a group without a mesh-sdk member says it has one: %v", v.Warnings) } } // 1297/B3a: the machines touched are those running a moved module or a consumer of what it provides; an added module // assigned nowhere touches none, and the verdict says so. func TestTouched(t *testing.T) { m := meshOf() if got := Touched(m, Change{Manifests: map[string]catalogue.Manifest{"store": m.Manifests["store"]}}); !slices.Equal(got, []string{"ace"}) { t.Errorf("touched by store = %v", got) } m.Assigned["g14"] = append(m.Assigned["g14"], "mail") if got := Touched(m, Change{Manifests: map[string]catalogue.Manifest{"store": m.Manifests["store"]}}); !slices.Equal(got, []string{"ace", "g14"}) { t.Errorf("touched by store with a consumer on g14 = %v", got) } // ADR 0559 §5: every node whose placement the change changes is touched, its file removed or not. if got := Touched(m, Change{Placement: &ChangePlacement{Placements: map[string]*delivery.Placement{"novox": nil, "shanks": {Node: "shanks"}}}}); !slices.Equal(got, []string{"novox", "shanks"}) { t.Errorf("touched by a placement = %v", got) } c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"new": {Module: "new", Version: "1"}}} if v := Compose(m, c, "sdk", 0); len(v.Machines) != 0 || !hasWarning(v, "touches no machine") || v.State != delivery.CompositionPass { t.Errorf("an added module assigned nowhere: %+v", v) } } // ADR 0537 clause 1: every verdict names, by machine, those that could run a moved or added module and are not assigned // it, and those whose grants alone the change alters (the bus's machine, a provider of what the module requires), as not // judged; shanks, providing what web now requires, is composed and so named in neither. A change that alters no grant // names none of the second. func TestTheMachinesNotJudgedAreNamed(t *testing.T) { m := meshOf() m.Manifests["bus"] = catalogue.Manifest{Module: "bus", Version: "1", Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: "mesh"}}} m.Manifests["db"] = catalogue.Manifest{Module: "db", Version: "1", Provides: []catalogue.Offer{{Name: "cache"}}} m.Assigned["novox"] = []string{"bus"} m.Assigned["shanks"] = []string{"db"} c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{ "web": {Module: "web", Version: "2", Reads: []string{"mesh-controller.assignments"}, Requires: []string{"cache"}}}} v := Compose(m, c, "sdk", 0) if !hasWarning(v, "could run a moved or added module and are not assigned it: ace, novox") || !slices.Contains(v.Machines, "shanks") { t.Errorf("the machines that could run web are not named: %v", v.Warnings) } if !hasWarning(v, "whose grants alone the change alters: novox") { t.Errorf("the machines whose grants alone change are not named: %v", v.Warnings) } c.Manifests["web"] = catalogue.Manifest{Module: "web", Version: "3"} if v := Compose(meshOf(), c, "sdk", 0); hasWarning(v, "grants alone") { t.Errorf("a change altering no grant names machines whose grants change: %v", v.Warnings) } }