package runtime import ( "encoding/json" "git.novox.be/novox/mesh-sdk/go/buscalls" "os" "slices" "sort" "strings" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/micro" "github.com/novox/mesh-tools/node-tools/internal/announce" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // novox/hq ADR 0197: what a runtime serves, it announces — the NATS services protocol's discovery, // read here with NATS's own types, each endpoint a subject actually served — and the announcement // follows a re-issued membership. func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) { mt.NeedBundles(t) mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil)) mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}})) nodeTools := connect(t, "node-tools", "anchor") stop, err := Run(nodeTools, []Served{ {"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}}, {"beta", []string{mt.Fixture("many-beta.serve.mjs")}}, }, nil, (&mt.Logs{}).Logf) if err != nil { t.Fatal(err) } defer stop() nc, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } defer nc.Close() info := func() micro.Info { t.Helper() msg, err := nc.Request("$SRV.INFO.node-tools.anchor", nil, 2*time.Second) if err != nil { t.Fatal(err) } var i micro.Info if err := json.Unmarshal(msg.Data, &i); err != nil { t.Fatal(err) } return i } subjects := func(i micro.Info) string { var out []string for _, e := range i.Endpoints { out = append(out, e.Subject+"|"+e.QueueGroup+"|"+e.Metadata["kind"]+"|"+e.Metadata["module"]+"|"+e.Metadata["seat"]+"|"+e.Metadata["scope"]+"|"+e.Metadata["interchangeable"]) } sort.Strings(out) return strings.Join(out, "\n") } got := info() if got.Type != micro.InfoResponseType || got.Name != "node-tools" || got.ID != "anchor" || got.Version == "" { t.Errorf("identity: %+v", got.ServiceIdentity) } want := strings.Join([]string{ "mesh.mod.alpha.tool.one.anchor||tool|alpha|||false", "mesh.mod.alpha.tool.two.anchor||tool|alpha|||false", "mesh.mod.beta.tool.five.anchor||tool|beta|||false", "mesh.mod.beta.tool.four.anchor||tool|beta|||false", "mesh.mod.beta.tool.three.anchor||tool|beta|||false", "mesh.seat.node-shelf.tool.clear.anchor||seat|beta|node-shelf|node|false", "mesh.seat.node-shelf.tool.list.anchor||seat|beta|node-shelf|node|false", }, "\n") if s := subjects(got); s != want { t.Errorf("announced:\n%s\nwant:\n%s", s, want) } for _, e := range got.Endpoints { if e.Metadata["node"] != "anchor" || e.Metadata["description"] == "" { t.Errorf("endpoint metadata: %+v", e) } // A seat's verb carries its schema; a module's tool does not — it outgrew the bus (2026-10-04). _, hasSchema := e.Metadata["schema"] if e.Metadata["kind"] == "seat" && !json.Valid([]byte(e.Metadata["schema"])) { t.Errorf("a seat's verb without a schema: %+v", e) } if e.Metadata["kind"] == "tool" && hasSchema { t.Errorf("a module's tool still announces its schema: %+v", e) } } // Every subject announced is answered. asker := connect(t, "console", "workstation") for _, e := range got.Endpoints { if _, err := asker.Ask("", map[string]any{}, e.Subject); err != nil { t.Errorf("announced %s and does not answer it: %v", e.Subject, err) } } // PING answers with the same identity, and a request for another service is not answered. if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) { t.Errorf("ping: %v %v", msg, err) } // alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable. mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil)) var after string for i := 0; i < 40; i++ { after = subjects(info()) if strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") { break } time.Sleep(100 * time.Millisecond) } if !strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") || !strings.Contains(after, "mesh.mod.alpha.tool.one.anchor||tool|alpha|||true") { t.Errorf("after a re-issued membership:\n%s", after) } _ = bus.Served{} } // issue 1966 TLS-3 (ENG-2): the runner states the capabilities of the calls it answers in its PING, on its own instance, which // is what its machine's node-engine asks and takes into the node's record; no bundle is needed for it. func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) { t.Setenv(FingerprintWord, "sha256:runner") mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil)) nodeTools := connect(t, "node-tools", "anchor") stop, err := Run(nodeTools, nil, nil, (&mt.Logs{}).Logf) if err != nil { t.Fatal(err) } defer stop() nc, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } defer nc.Close() msg, err := nc.Request("$SRV.PING.node-tools.anchor", nil, 2*time.Second) if err != nil { t.Fatalf("ping of the instance: %v", err) } var ping micro.Ping if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "approval-mark/1,bus-state-calls,finding-calls,ledger-calls,ledger-upkeep-calls,put-sessions,seat-settle-delay,sent-objects-calls" { t.Errorf("the runner states %q (%v), want its bus-state and ledger calls", ping.Metadata[announce.Capabilities], err) } // Its own fingerprint beside them (design 46 approval mark §6). if ping.Metadata[announce.Fingerprint] != "sha256:runner" { t.Errorf("the runner states its fingerprint as %q", ping.Metadata[announce.Fingerprint]) } } // Design 47 G §462, §488, §622: each call is stated under the capability word of its own family, the words mesh-sdk's buscalls // names: the read of a sent object under sent-objects-calls (no longer under bus-state-calls), the ledger's fill and removal // under ledger-upkeep-calls, the others where they were. func TestEachCallIsStatedUnderTheWordOfItsFamily(t *testing.T) { for method, word := range map[string]string{ buscalls.ObjectsSentGet: buscalls.SentObjectsCalls, buscalls.LedgerFill: buscalls.LedgerUpkeepCalls, buscalls.LedgerRemove: buscalls.LedgerUpkeepCalls, buscalls.LedgerApply: buscalls.LedgerCalls, buscalls.ObjectsPut: buscalls.BusStateCalls, } { var in []string for c, methods := range callFamilies { if slices.Contains(methods, method) { in = append(in, c) } } if len(in) != 1 || in[0] != word { t.Errorf("%s is stated under %v, want %s alone", method, in, word) } } } // Design 47 G §545: the words the tool runner states are exactly its manifest's artifact `states`, so the delivery plan's edge // of §605 runs to it for each word a module's artifact `calls`. func TestTheWordsTheRunnerStatesAreItsManifestsStates(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { Build struct { Artifacts []struct { Name string `json:"name"` States []string `json:"states"` } `json:"artifacts"` } `json:"build"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } var declared []string for _, a := range m.Build.Artifacts { if a.Name == "runtime" { declared = a.States } } slices.Sort(declared) if got := CallCapabilities(); !slices.Equal(got, declared) { t.Errorf("the runner states %v, its manifest's states are %v", got, declared) } }