package runtime import ( "encoding/json" "strings" "sync" "testing" "time" "github.com/nats-io/nats.go" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // A module's code reads its values at runtime through moduleBus.Provision and ProvisionWatch (novox/hq ADR 0349 // ยง14, ADR 0370, issue 685), against a real bus with a stand-in for the machine's provisioner client: the runtime // asks as the module whose bundle asks, whatever the bundle names; a watch hands the current value, then each change // the client says moved, and a refusal once; a secret is refused without asking. // standInClient answers prov.ask. from its values, keyed `..`, and keeps every ask. type standInClient struct { mu sync.Mutex values map[string]string asks []string nc *nats.Conn } func aStandInClient(t *testing.T, node string, values map[string]string) *standInClient { t.Helper() nc, err := nats.Connect(mt.URL(t)) if err != nil { t.Fatal(err) } t.Cleanup(nc.Close) c := &standInClient{values: values, nc: nc} if _, err := nc.Subscribe("prov.ask."+node, func(m *nats.Msg) { var a struct{ Module, Kind, Key string } _ = json.Unmarshal(m.Data, &a) c.mu.Lock() c.asks = append(c.asks, string(m.Data)) v, ok := c.values[a.Module+"."+a.Kind+"."+a.Key] c.mu.Unlock() if !ok { _ = m.Respond([]byte(`{"error":"` + a.Module + ` declares no config value ` + a.Key + `"}`)) return } _ = m.Respond([]byte(`{"value":` + v + `}`)) }); err != nil { t.Fatal(err) } _ = nc.Flush() return c } func (c *standInClient) set(key, value string) { c.mu.Lock() defer c.mu.Unlock() if value == "" { delete(c.values, key) return } c.values[key] = value } func (c *standInClient) asked() []string { c.mu.Lock() defer c.mu.Unlock() return append([]string(nil), c.asks...) } func (c *standInClient) say(t *testing.T, node, module, body string) { t.Helper() if err := c.nc.Publish("prov.changed."+node+"."+module, []byte(body)); err != nil { t.Fatal(err) } } func TestABundleAsksForItsValuesAsItsOwnModule(t *testing.T) { client := aStandInClient(t, "anchor", map[string]string{ "claude-code.config.managed_settings": `{"spinnerTipsEnabled":false}`, "dunst.config.managed_settings": `"dunst's"`, }) conn := connect(t, "node-tools", "anchor") all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} b := all.forModule("claude-code").(*moduleBus) // Its own value, though the bundle names another module: the runtime names the module from the bundle. got, err := b.Provision("get", json.RawMessage(`{"kind":"config","key":"managed_settings","module":"dunst"}`)) if err != nil || string(got) != `{"spinnerTipsEnabled":false}` { t.Fatalf("answered %s, %v", got, err) } if asks := client.asked(); len(asks) != 1 || asks[0] != `{"key":"managed_settings","kind":"config","module":"claude-code"}` { t.Fatalf("the client was asked %v", asks) } // The client's refusal, in its words. if _, err := b.Provision("get", json.RawMessage(`{"kind":"config","key":"nothing"}`)); err == nil || !strings.Contains(err.Error(), "declares no config value nothing") { t.Errorf("an undeclared key: %v", err) } // A secret, refused here and never asked; and what is not an ask. before := len(client.asked()) for params, want := range map[string]string{ `{"kind":"secret","key":"gitea-read"}`: "the vault hands a secret only to a caller the bus proves", `{"kind":"setting","key":"role"}`: "not a kind of value", `{"key":"role"}`: "names no value", } { if _, err := b.Provision("get", json.RawMessage(params)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: %v, want %q", params, err, want) } } if _, err := b.ProvisionWatch(json.RawMessage(`{"kind":"secret","key":"gitea-read"}`), func(json.RawMessage) error { return nil }); err == nil { t.Error("a secret was watched") } if n := len(client.asked()); n != before { t.Errorf("the client was asked for what the runtime refuses: %v", client.asked()[before:]) } } func TestABundlesWatchIsHandedTheValueAndEachChange(t *testing.T) { client := aStandInClient(t, "anchor", map[string]string{"claude-code.config.managed_settings": `{"a":1}`}) conn := connect(t, "node-tools", "anchor") all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}} t.Cleanup(all.stopAll) b := all.forModule("claude-code").(*moduleBus) handed := make(chan ProvisionChange, 16) stop, err := b.ProvisionWatch(json.RawMessage(`{"kind":"config","key":"managed_settings","module":"dunst"}`), func(raw json.RawMessage) error { var c ProvisionChange if err := json.Unmarshal(raw, &c); err != nil { t.Errorf("handed %s", raw) } handed <- c return nil }) if err != nil { t.Fatal(err) } next := func(what string) ProvisionChange { t.Helper() select { case c := <-handed: return c case <-time.After(5 * time.Second): t.Fatalf("nothing handed: %s", what) } return ProvisionChange{} } nothing := func(what string) { t.Helper() select { case c := <-handed: t.Errorf("%s: handed %+v", what, c) case <-time.After(300 * time.Millisecond): } } if c := next("the current value"); string(c.Value) != `{"a":1}` || c.Kind != "config" || c.Key != "managed_settings" { t.Fatalf("first handed %+v", c) } // A change, said by the client: asked again, handed. client.set("claude-code.config.managed_settings", `{"a":2}`) client.say(t, "anchor", "claude-code", `{"kind":"config","key":"managed_settings"}`) if c := next("the change"); string(c.Value) != `{"a":2}` { t.Fatalf("after the change, handed %+v", c) } // Every key of the kind said to have moved, the value the same: nothing handed. Another module's or another // key's word: not even asked. client.say(t, "anchor", "claude-code", `{"kind":"config"}`) nothing("the same value again") before := len(client.asked()) client.say(t, "anchor", "dunst", `{"kind":"config","key":"managed_settings"}`) client.say(t, "anchor", "claude-code", `{"kind":"config","key":"role"}`) nothing("another's word") if n := len(client.asked()); n != before { t.Errorf("asked again for another's word: %v", client.asked()[before:]) } // No longer answered: handed the refusal, once. client.set("claude-code.config.managed_settings", "") client.say(t, "anchor", "claude-code", `{"kind":"config"}`) if c := next("the refusal"); c.Refused == "" || len(c.Value) != 0 { t.Fatalf("handed %+v, want the refusal", c) } client.say(t, "anchor", "claude-code", `{"kind":"config"}`) nothing("the same refusal again") // Answered again, and then stopped: nothing after. client.set("claude-code.config.managed_settings", `{"a":3}`) client.say(t, "anchor", "claude-code", `{"kind":"config"}`) if c := next("answered again"); string(c.Value) != `{"a":3}` { t.Fatalf("handed %+v", c) } stop() client.set("claude-code.config.managed_settings", `{"a":4}`) client.say(t, "anchor", "claude-code", `{"kind":"config"}`) nothing("a stopped watch") for _, a := range client.asked() { if !strings.Contains(a, `"module":"claude-code"`) { t.Errorf("asked as another module: %s", a) } } }