package catalogue import ( "encoding/json" "reflect" "slices" "strings" "testing" ) // The one rule replays the live cases of 2026-10-30 (issue 774): what the assigner's rule 8 waits for is what the // composition keys, judged from the manifests alone. func TestAwaitedIsWhatTheCompositionKeys(t *testing.T) { built := &Handed{Source: "store://t", Digest: "sha256:t", Templates: map[string]string{"c": "c"}} offer := func(module, name string, scope string, helper bool, serves bool) Manifest { o := Offer{Name: name, Scope: scope} if helper { o.AnsweredBy = AnsweredByHelper } m := Manifest{Module: module, Provides: []Offer{o}} if serves { m.Serves = map[string]map[string]any{name: {"port": 1}} } return m } gitea, distribution := offer("gitea", "forge-account", ScopeMesh, false, false), offer("distribution", "artifact-store", ScopeMesh, false, false) goTools, nodeJS := offer("go", "go-toolchain", ScopeNode, false, false), offer("nodejs", "node-toolchain", ScopeNode, false, false) keycloak, proxy := offer("keycloak", "oidc-client", ScopeMesh, false, false), offer("route-proxy", "route", ScopeMesh, true, false) adapter := offer("route-adapter", "route", ScopeMesh, false, false) ollama := offer("ollama", "llm", ScopeNode, false, true) by := func(ms ...Manifest) func(string) []Manifest { return func(p string) []Manifest { var out []Manifest for _, m := range ms { if slices.Contains(m.Offers(), p) { out = append(out, m) } } return out } } for _, c := range []struct { name string consumer Manifest providers func(string) []Manifest want []string }{ {"a handed builder waits for what binds, not for a toolchain on its own machine (mesh-build-go on ace)", Manifest{Module: "mesh-build-go", Moved: true, Handed: built, Requires: []string{"forge-account", "go-toolchain", "artifact-store", "node-tools"}}, by(gitea, distribution, goTools), []string{"artifact-store", "forge-account"}}, {"node-toolchain alike (mesh-build-node)", Manifest{Module: "mesh-build-node", Moved: true, Handed: built, Requires: []string{"node-toolchain", "forge-account"}}, by(gitea, nodeJS), []string{"forge-account"}}, {"a module not handed waits for its route, which the helper answers, and not for its login the controller still makes (grafana on ace)", Manifest{Module: "grafana", Requires: []string{"route", "oidc-client", "influxdb-api"}, OptionalRequires: map[string]bool{"influxdb-api": true}}, by(keycloak, proxy), []string{"route"}}, {"where providers disagree on the helper, nothing is waited for that might never be keyed", Manifest{Module: "grafana", Requires: []string{"route"}}, by(proxy, adapter), nil}, {"a moved module with no template is not handed (a held stub, issue 1126)", Manifest{Module: "bot", Moved: true, Handed: &Handed{}, Requires: []string{"oidc-client", "route"}}, by(keycloak, proxy), []string{"route"}}, {"a provision on its own machine that serves facts binds, and is keyed for a handed consumer", Manifest{Module: "chat", Moved: true, Handed: built, Requires: []string{"llm", "llm"}}, by(ollama), []string{"llm"}}, } { if got := Awaited(c.consumer, c.providers); !reflect.DeepEqual(got, c.want) { t.Errorf("%s: awaited %v, want %v", c.name, got, c.want) } } } // A module not handed has a plan for what a provider's helper answers of it, and for nothing else (issue 774: grafana // had no plan, so no route key, and route-proxy served no route for it); a need the controller still answers stays out. func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) { consumer := notifierConsumer(t, false, false) consumer.Requires = append(consumer.Requires, "route") consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}} consumer.ContributesMany = map[string]map[string]map[string]any{"route": {"api": {"label": "notify-api", "port": 9000}}} node := workstation() node.At, node.PublicDomain = "workstation.internal", "example.org" proxy := Manifest{Module: "proxy", Version: "1", Provides: []Offer{{Name: "route", Scope: ScopeMesh, AnsweredBy: AnsweredByHelper}}} store := Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh, Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}} w := twoStores("anchor") w.Offered["route"] = []Provider{{Node: "anchor", At: "anchor.internal", Module: "proxy"}} r, err := Resolve(shelf(consumer, proxy, store), []string{consumer.Module}, node, w) if err != nil { t.Fatal(err) } r.Account = "op" for i, n := range r.Needs { // As the plan sets it (compose's answeredByHelper). r.Needs[i].AnsweredByHelper = n.Name == "route" } _, before := composeWith(t, r, Rendering{}) c, got := composeWith(t, r, handingTo("store:5000")) if got != before { t.Fatalf("the plan changed what is composed:\nbefore: %s\nnow: %s", before, got) } plan, ok := c.Plans["notify"] if !ok || len(plan.Provisions) != 1 || plan.Provisions[0].Provision != "route" || plan.Provisions[0].ProviderModule != "proxy" { t.Fatalf("the plan is %+v (%v), want the route alone", plan, ok) } if plan.Templates != (PlanTemplates{}) || plan.Files != nil || plan.Secrets != nil || !strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" { t.Fatalf("the plan names %+v", plan) } // What it contributes, composed as a received file carried it, and where its machine is, as opaque bytes the // provider's own contract reads (issue 774, research 072 row 34): route-proxy serves the route from the plan alone. given, err := DecodeContributed(plan.Provisions[0].Contributed) if err != nil || given.At != "workstation.internal" || len(given.Each) != 2 || given.Each[0].Local != "" || string(given.Each[0].Values) != `{"host":"notify.example.org"}` || given.Each[1].Local != "api" || string(given.Each[1].Values) != `{"internal-name":"notify-api.anchor.internal","label":"notify-api","name":"notify-api.example.org","port":9000}` { t.Fatalf("the plan gives %+v (%v)", given, err) } // Its port is where the machine publishes it, the number ${port:9000} gives the module and its declaration publishes, // never the software's own where the placement moved it (2026-10-31: route-proxy dialled gitea's 3000, published at // 20000): its provider dials what the plan says. Where a plan carries it, a port the module does not listen on is // refused by module, node and port. for _, p := range []struct { name string ports map[string]map[int]int given map[string]map[int]int planned bool port float64 refused string }{ {"assigned by the placement (gitea 3000 at 20000)", map[string]map[int]int{"notify": {9000: 20009}}, nil, true, 20009, ""}, {"given by the node over the placement", map[string]map[int]int{"notify": {9000: 20009}}, map[string]map[int]int{"notify": {9000: 9900}}, true, 9900, ""}, {"a bare published port the placement holds none for keeps its number (influxdb 8086)", map[string]map[int]int{"notify": {9000: 9000}}, nil, true, 9000, ""}, {"a mapping the module fixed (as mailu 7080:80)", map[string]map[int]int{"notify": {9000: 7080}}, nil, true, 7080, ""}, {"a port the module does not listen on", nil, nil, true, 0, "notify contributing to route: its port 9000 on workstation is none of the ports it listens on"}, {"a declaration with no ports leaves it as written", nil, nil, false, 9000, ""}, } { with := handingTo("store:5000") with.Ports, with.Given, with.Planned = p.ports, p.given, p.planned c, err := r.Compose(with) if p.refused != "" { if err == nil || !strings.Contains(err.Error(), p.refused) { t.Errorf("%s: composed with %v, want refused with %q", p.name, err, p.refused) } continue } if err != nil { t.Errorf("%s: %v", p.name, err) continue } given, err := DecodeContributed(c.Plans["notify"].Provisions[0].Contributed) var api map[string]any if err == nil { err = json.Unmarshal(given.Each[1].Values, &api) } if err != nil || api["port"] != p.port || given.At != "workstation.internal" { t.Errorf("%s: the plan gives %v at %q (%v), want port %v", p.name, api, given.At, err, p.port) } } // Read strictly: a form the SDK does not write is refused, whatever its contributions say. for raw, refused := range map[string]string{ `{"v":2,"each":[{"values":{}}]}`: "version 2", `{"v":1,"each":[]}`: "no contribution", `{"v":1,"each":[{"values":{}}],"route":{}}`: "unknown field", `{"v":1,"each":[{"values":{}},{"values":{}}]}`: "twice", `{"v":1,"each":[{"local":"api","values":["a"]}]}`: "not a JSON object", `{"v":1,"each":[{"values":{}}]} {}`: "more than one", `{"v":1,"at":"10.0.0.2","each":[{"values":{"x":true}}]}`: "", } { _, err := DecodeContributed(json.RawMessage(raw)) if (refused == "") != (err == nil) || err != nil && !strings.Contains(err.Error(), refused) { t.Errorf("DecodeContributed(%s) = %v, want %q", raw, err, refused) } } // And it hands nothing: Plan.Hands, the one rule every reader of a plan asks, by each part a plan can name. if plan.Hands() { t.Fatalf("a plan naming the route alone hands its module: %+v", plan) } for _, c := range []struct { name string plan Plan hands bool }{ {"empty", Plan{}, false}, {"provisions alone", Plan{Provisions: plan.Provisions}, false}, {"a digest without an archive", Plan{Templates: PlanTemplates{Digest: "sha256:1"}}, false}, {"templates alone", Plan{Templates: PlanTemplates{Source: "notify.tar", Digest: "sha256:1"}}, true}, {"files alone", Plan{Files: []PlanFile{{ID: "conf", Path: "/etc/notify.conf"}}}, true}, {"secrets alone", Plan{Secrets: []PlanSecret{{Name: "admin"}}}, true}, {"items alone", Plan{Items: []PlanItem{{Collection: "routes", Template: "route.tmpl"}}}, true}, {"secrets and provisions", Plan{Secrets: []PlanSecret{{Name: "admin"}}, Provisions: plan.Provisions}, true}, } { if got := c.plan.Hands(); got != c.hands { t.Errorf("%s: Hands() is %v, want %v", c.name, got, c.hands) } } if want := Awaited(consumer, func(p string) []Manifest { return map[string][]Manifest{"route": {proxy}, "postgres-database": {store}}[p] }); !reflect.DeepEqual(want, []string{"route"}) { t.Fatalf("the assigner waits for %v, and the plan keys the route alone", want) } }