package runtime import ( "encoding/json" "os" "strings" "testing" "time" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) func connect(t *testing.T, module, node string) *bus.Conn { t.Helper() c, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: module, Node: node}) if err != nil { t.Fatal(err) } c.Logf = func(string, ...any) {} t.Cleanup(c.Close) return c } func call(t *testing.T, asker *bus.Conn, key string, args any) (string, error) { t.Helper() got, err := asker.Ask(key, args, "") return string(got.Result), err } func same(t *testing.T, got, want string) { t.Helper() var a, b any if json.Unmarshal([]byte(got), &a) != nil || json.Unmarshal([]byte(want), &b) != nil { t.Fatalf("not JSON: got %s want %s", got, want) } ga, _ := json.Marshal(a) gb, _ := json.Marshal(b) if string(ga) != string(gb) { t.Errorf("got %s, want %s", got, want) } } // The node's runtime serves five modules' bundles on one credential — two TypeScript, one broken, // one Python, one written against the protocol — and follows a re-issued membership (ADR 0175, 0193). func TestTheNodesRuntimeServesFiveModulesAndFollowsAReissuedMembership(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil)) mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}})) mesh.Issue(t, mt.MembershipOf("gamma", "anchor", false, nil)) mesh.Issue(t, mt.MembershipOf("delta", "anchor", false, map[string][]string{"node-lamp": {"on"}})) mesh.Issue(t, mt.MembershipOf("epsilon", "anchor", false, nil)) nodeTools := connect(t, "node-tools", "anchor") asker := connect(t, "console", "workstation") logs := &mt.Logs{} t.Setenv("MESH_OPERATOR_ACCOUNT", "somebody") t.Setenv("MESH_OPERATOR_HOME", "/home/somebody") stop, err := Run(nodeTools, []Served{ {"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}}, {"beta", []string{mt.Fixture("many-beta.serve.mjs")}}, {"gamma", []string{mt.Fixture("many-broken.serve.mjs")}}, {"delta", []string{mt.Fixture("many-delta.py")}}, {"epsilon", []string{mt.Fixture("many-epsilon.mjs")}}, }, nil, logs.Logf) if err != nil { t.Fatal(err) } defer stop() if !logs.Has("the operator's account here is somebody (home /home/somebody)") { t.Errorf("the operator was not said:\n%s", logs.All()) } if !logs.Has("gamma's bundle", "many-broken.serve.mjs failed to load: gamma's bundle exited (1): Error: gamma's bundle cannot find its client; its tools are not served here") { t.Errorf("the broken bundle was not named with its own words:\n%s", logs.All()) } if !logs.Has("serving 8 tool(s) for 5 module(s): alpha.one, alpha.two, beta.three, beta.four, beta.five, delta.greet, delta.die, epsilon.seven; not serving gamma") { t.Errorf("not serving what it should:\n%s", logs.All()) } for key, want := range map[string]string{ "alpha.one": `{"alpha":1}`, "alpha.one@anchor": `{"alpha":1}`, "beta.three@anchor": `{"beta":3}`, "beta.four@anchor": `{"beta":4}`, "beta.five@anchor": `{"beta":5}`, "seat:node-shelf.list@anchor": `{"shelf":["a","b"]}`, "seat:node-shelf.clear@anchor": `{"cleared":true}`, "seat:node-lamp.on@anchor": `{"on":true,"language":"python"}`, "epsilon.seven@anchor": `{"epsilon":7,"via":"stdio"}`, } { got, err := call(t, asker, key, map[string]any{}) if err != nil { t.Errorf("%s: %v", key, err) continue } same(t, got, want) } if _, err := call(t, asker, "beta.three", map[string]any{}); err == nil || !strings.Contains(err.Error(), "no responders") { t.Errorf("beta was not issued the plain subject, and answered on it: %v", err) } got, err := call(t, asker, "delta.greet@anchor", map[string]any{"who": "mesh"}) if err != nil { t.Fatal(err) } same(t, got, `{"greeting":"hello mesh","language":"python"}`) // A launched tool that emits does so as its module, through the runtime. landed := mesh.NextEvent(t, "mesh.mod.*.event.>") got, err = call(t, asker, "alpha.two", map[string]any{}) if err != nil { t.Fatal(err) } same(t, got, `{"alpha":2}`) select { case subject := <-landed: if subject != "mesh.mod.alpha.event.happened" { t.Errorf("the event landed on %s", subject) } case <-time.After(5 * time.Second): t.Error("the event never landed") } // A bundle that dies mid-call is said, and started again on its next call. if _, err := call(t, asker, "delta.die@anchor", map[string]any{}); err == nil { t.Error("a bundle that died answered") } got, err = call(t, asker, "delta.greet@anchor", map[string]any{"who": "again"}) if err != nil { t.Fatalf("not started again: %v", err) } same(t, got, `{"greeting":"hello again","language":"python"}`) // `tools` answers for each, and why gamma serves nothing. gamma, err := call(t, asker, "gamma.tools@anchor", map[string]any{}) if err != nil { t.Fatal(err) } same(t, gamma, `{"module":"gamma","tools":[],"failed":"gamma's bundle exited (1): Error: gamma's bundle cannot find its client"}`) beta, _ := call(t, asker, "beta.tools@anchor", map[string]any{}) var answer ToolsAnswer _ = json.Unmarshal([]byte(beta), &answer) if len(answer.Tools) != 3 || answer.Tools[0].Name != "three" || strings.Join(answer.Tools[0].Subjects, ",") != "mesh.mod.beta.tool.three.anchor" { t.Errorf("beta's tools answer: %s", beta) } // Re-issued mid-run, now answering for the module anywhere: served without a restart. mesh.Issue(t, mt.MembershipOf("beta", "anchor", true, map[string][]string{"node-shelf": {"list", "clear"}})) mt.Until(t, func() error { _, err := call(t, asker, "beta.three", map[string]any{}); return err }) got, err = call(t, asker, "seat:node-shelf.list@anchor", map[string]any{}) if err != nil { t.Fatal(err) } same(t, got, `{"shelf":["a","b"]}`) } // Each bundle is given its own environment and none of another's (ADR 0192), and the composed // environments are not left in the runtime's. func TestEachBundleIsGivenItsOwnEnvironment(t *testing.T) { mesh := mt.New(t) for _, m := range []string{"gamma", "delta", "zeta"} { mesh.Issue(t, mt.MembershipOf(m, "anchor", false, nil)) } nodeTools := connect(t, "node-tools", "anchor") asker := connect(t, "console", "workstation") t.Setenv("MESH_OPERATOR_ACCOUNT", "somebody") t.Setenv(ToolEnv, `{"gamma":{"GAMMA_CONFIG_FILE":"/var/lib/mesh/gamma/config.json"},"delta":{"DELTA_TOKEN_FILE":"/var/lib/mesh/delta/token"},"zeta":{"ZETA_URL":"http://127.0.0.1:3000"}}`) envs, err := TakeToolEnvs() if err != nil { t.Fatal(err) } if _, left := os.LookupEnv(ToolEnv); left { t.Error("the composed environments were left in the runtime's") } logs := &mt.Logs{} stop, err := Run(nodeTools, []Served{ {"gamma", []string{mt.Fixture("env-gamma.serve.mjs")}}, {"delta", []string{mt.Fixture("env-delta.serve.mjs")}}, {"zeta", []string{mt.Fixture("env-zeta.mjs")}}, }, envs, logs.Logf) if err != nil { t.Fatal(err) } defer stop() for key, want := range map[string]string{ "gamma.given@anchor": `{"mine":"/var/lib/mesh/gamma/config.json","theirs":null,"runtime":"somebody","composed":null}`, "delta.given@anchor": `{"mine":"/var/lib/mesh/delta/token","theirs":null}`, "zeta.given@anchor": `{"mine":"http://127.0.0.1:3000","theirs":null,"composed":null}`, } { got, err := call(t, asker, key, map[string]any{}) if err != nil { t.Fatalf("%s: %v\n%s", key, err, logs.All()) } same(t, got, want) } } // An entrypoint that is not executable is refused by name, and the others serve (ADR 0193). func TestAnEntrypointThatIsNotExecutableIsRefused(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil)) mesh.Issue(t, mt.MembershipOf("plain", "anchor", false, nil)) nodeTools := connect(t, "node-tools", "anchor") asker := connect(t, "console", "workstation") logs := &mt.Logs{} stop, err := Run(nodeTools, []Served{ {"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}}, {"plain", []string{mt.Fixture("many-alpha.mjs")}}, }, nil, logs.Logf) if err != nil { t.Fatal(err) } defer stop() if !logs.Has("plain's bundle", "many-alpha.mjs failed to load:", "is not executable; a bundle the runtime serves is started, never imported") { t.Errorf("the non-executable entrypoint was not refused by name:\n%s", logs.All()) } got, err := call(t, asker, "alpha.one@anchor", map[string]any{}) if err != nil { t.Fatal(err) } same(t, got, `{"alpha":1}`) } // A launched bundle is told the module it serves, so its seat's verbs stay the seat's (ADR 0193). func TestALaunchedBundleRegisteringItsSeatFirstServesTheSeat(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("theta", "anchor", false, map[string][]string{"node-shelf": {"list"}})) nodeTools := connect(t, "node-tools", "anchor") asker := connect(t, "console", "workstation") stop, err := Run(nodeTools, []Served{{"theta", []string{mt.Fixture("served-seat-first.mjs")}}}, map[string]map[string]string{"theta": {"THETA_WORD": "given"}}, (&mt.Logs{}).Logf) if err != nil { t.Fatal(err) } defer stop() got, err := call(t, asker, "theta.own@anchor", map[string]any{}) if err != nil { t.Fatal(err) } same(t, got, `{"theta":"given"}`) got, err = call(t, asker, "seat:node-shelf.list@anchor", map[string]any{}) if err != nil { t.Fatal(err) } same(t, got, `{"shelf":["x"]}`) } func TestToolModulesNamesOtherModulesOnly(t *testing.T) { got, err := ServedModulesFrom(" alpha=/a/tools/index.serve.mjs, beta=/b/one, beta=/b/two ", "node-tools") if err != nil || len(got) != 2 || got[1].Module != "beta" || len(got[1].Entrypoints) != 2 { t.Fatalf("%v %v", got, err) } for _, bad := range []string{"/mine/index.js", "node-tools=/own.js", "=/x"} { if _, err := ServedModulesFrom(bad, "node-tools"); err == nil { t.Errorf("%q was accepted", bad) } } }