The assigner's rule 8 waited for a key for every `requires`, while the plans named only the needs of handed modules: every new build of grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited for a key nobody would make, and grafana, not handed, had no plan, so no route key and no route on route-proxy. catalogue.Keyed is the rule (a need a module answers is keyed when its consumer is handed or the provider's helper answers it), catalogue.Awaited the same rule from manifests for the assigner, and a module not handed now has a plan for what a helper answers of it. Handed modules' plans are unchanged. Quick fix: drop the missing-key check from rule 8. Thorough fix (chosen): one rule in mesh-sdk all three parts use, since the kind (three parts deciding the same set apart) already drifted once and hid failures as waits.
113 lines
5.5 KiB
Go
113 lines
5.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"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"}}
|
|
node := workstation()
|
|
node.At = "workstation.internal"
|
|
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)
|
|
}
|
|
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)
|
|
}
|
|
}
|