mesh/merge-gate head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] t…
mesh/repo-check head d3e182503c19 merged into trunk c8b07242a009 — pass: [release feat/57-config-and-provisioning, judged by its controller fcdecb436117] i…
mesh/delivery delivered
Providers, the state bus, templates and the environment collection that the feature's modules build on (feature 57, config and provisioning).
177 lines
6.7 KiB
Go
177 lines
6.7 KiB
Go
package stdio
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A Go bundle reads its values at runtime through the runtime (novox/hq ADR 0349 §14, ADR 0370, issue 685): Config,
|
|
// Bound and their watches ask the runtime, which asks as the module; a watch's value now is handled before it
|
|
// returns and each change after is named by the watch's id; what the module does not declare and every secret come
|
|
// back refused, in the runtime's words; a change for no watch of this bundle is refused.
|
|
func TestABundleReadsAndWatchesItsValuesThroughTheRuntime(t *testing.T) {
|
|
t.Setenv(ServedAs, "claude-code")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
seen := make(chan string, 16)
|
|
watched := make(chan error, 1)
|
|
answers := make(chan string, 4)
|
|
go func() {
|
|
_ = ServeOn("", []Tool{
|
|
{Name: "start", Run: func(map[string]any) (any, error) {
|
|
go func() {
|
|
watched <- WatchConfig("managed_settings", func(c ProvisionChange) error {
|
|
seen <- c.Kind + " " + c.Key + " " + string(c.Value) + c.Refused
|
|
return nil
|
|
})
|
|
}()
|
|
return "started", nil
|
|
}},
|
|
{Name: "read", Run: func(a map[string]any) (any, error) {
|
|
var v json.RawMessage
|
|
var err error
|
|
switch a["what"] {
|
|
case "config":
|
|
v, err = Config(a["key"].(string))
|
|
case "bound":
|
|
v, err = Bound("mcp-endpoint", "url")
|
|
case "secret":
|
|
var s []byte
|
|
s, err = Secret("gitea-read")
|
|
v = json.RawMessage(s)
|
|
}
|
|
if err != nil {
|
|
answers <- "refused: " + err.Error()
|
|
} else {
|
|
answers <- string(v)
|
|
}
|
|
return "read", nil
|
|
}},
|
|
}, inR, outW)
|
|
}()
|
|
lines := bufio.NewScanner(outR)
|
|
next := func() map[string]any {
|
|
done := make(chan map[string]any, 1)
|
|
go func() {
|
|
if lines.Scan() {
|
|
var m map[string]any
|
|
_ = json.Unmarshal(lines.Bytes(), &m)
|
|
done <- m
|
|
}
|
|
}()
|
|
select {
|
|
case m := <-done:
|
|
return m
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("the bundle said nothing")
|
|
return nil
|
|
}
|
|
}
|
|
send := func(m map[string]any) { b, _ := json.Marshal(m); _, _ = inW.Write(append(b, '\n')) }
|
|
// asked is the bundle's next ask of the runtime, beside its answers to this tool call and those before.
|
|
asked := func(callID int) map[string]any {
|
|
t.Helper()
|
|
var ask map[string]any
|
|
for ask == nil {
|
|
m := next()
|
|
if m["method"] != nil {
|
|
ask = m
|
|
} else if id, ok := m["id"].(float64); !ok || id > float64(callID) || m["error"] != nil {
|
|
t.Fatalf("said %v", m)
|
|
}
|
|
}
|
|
return ask
|
|
}
|
|
|
|
// The watch: asked of the runtime with the kind and the key and no module, the module being the runtime's word.
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "start"}})
|
|
watch := asked(1)
|
|
if watch["method"] != "mesh/provision.watch" {
|
|
t.Fatalf("asked %v", watch)
|
|
}
|
|
if p := watch["params"].(map[string]any); p["kind"] != "config" || p["key"] != "managed_settings" || len(p) != 2 {
|
|
t.Fatalf("asked to watch %v", p)
|
|
}
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 50, "method": "mesh/provision", "params": map[string]any{"watch": watch["id"],
|
|
"change": map[string]any{"kind": "config", "key": "managed_settings", "value": map[string]any{"spinnerTipsEnabled": false}}}})
|
|
for {
|
|
if r := next(); r["id"] == float64(50) {
|
|
if r["error"] != nil {
|
|
t.Fatalf("the value now was answered %v", r)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if s := <-seen; s != `config managed_settings {"spinnerTipsEnabled":false}` {
|
|
t.Fatalf("handled %q", s)
|
|
}
|
|
select {
|
|
case err := <-watched:
|
|
t.Fatalf("the watch returned before it was answered: %v", err)
|
|
default:
|
|
}
|
|
send(map[string]any{"jsonrpc": "2.0", "id": watch["id"], "result": map[string]any{}})
|
|
if err := <-watched; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A change, and then the value no longer answered.
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 51, "method": "mesh/provision", "params": map[string]any{"watch": watch["id"],
|
|
"change": map[string]any{"kind": "config", "key": "managed_settings", "value": map[string]any{"spinnerTipsEnabled": true}}}})
|
|
if r := next(); r["id"] != float64(51) || r["error"] != nil {
|
|
t.Fatalf("a change was answered %v", r)
|
|
}
|
|
if s := <-seen; s != `config managed_settings {"spinnerTipsEnabled":true}` {
|
|
t.Fatalf("handled %q", s)
|
|
}
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 52, "method": "mesh/provision", "params": map[string]any{"watch": watch["id"],
|
|
"change": map[string]any{"kind": "config", "key": "managed_settings", "refused": "claude-code is not placed on g14"}}})
|
|
if r := next(); r["id"] != float64(52) || r["error"] != nil {
|
|
t.Fatalf("a refusal was answered %v", r)
|
|
}
|
|
if s := <-seen; s != `config managed_settings claude-code is not placed on g14` {
|
|
t.Fatalf("handled %q", s)
|
|
}
|
|
// A change for a watch this bundle never asked for is refused.
|
|
send(map[string]any{"jsonrpc": "2.0", "id": 53, "method": "mesh/provision", "params": map[string]any{"watch": "bundle-999",
|
|
"change": map[string]any{"kind": "config", "key": "x"}}})
|
|
if r := next(); r["id"] != float64(53) || !strings.Contains(r["error"].(map[string]any)["message"].(string), "names no watch") {
|
|
t.Fatalf("a change for no watch was answered %v", r)
|
|
}
|
|
|
|
// Each reader asks the runtime, and hands back its answer or its refusal.
|
|
for i, c := range []struct {
|
|
what, key, kind, askedKey string
|
|
result any
|
|
refusal string
|
|
want string
|
|
}{
|
|
{what: "config", key: "role", kind: "config", askedKey: "role", result: "the build machine", want: `"the build machine"`},
|
|
{what: "config", key: "nothing", kind: "config", askedKey: "nothing",
|
|
refusal: "claude-code declares no config value nothing", want: "refused: claude-code declares no config value nothing"},
|
|
{what: "bound", kind: "bound", askedKey: "mcp-endpoint:url", result: "http://127.0.0.1:9000", want: `"http://127.0.0.1:9000"`},
|
|
{what: "secret", kind: "secret", askedKey: "gitea-read",
|
|
refusal: "the vault hands a secret only to a caller the bus proves", want: "refused: the vault hands a secret only to a caller the bus proves"},
|
|
} {
|
|
id := 10 + i
|
|
send(map[string]any{"jsonrpc": "2.0", "id": id, "method": "tools/call", "params": map[string]any{"name": "read",
|
|
"arguments": map[string]any{"what": c.what, "key": c.key}}})
|
|
get := asked(id)
|
|
if p := get["params"].(map[string]any); get["method"] != "mesh/provision.get" || p["kind"] != c.kind || p["key"] != c.askedKey || len(p) != 2 {
|
|
t.Fatalf("%s: asked %v", c.what, get)
|
|
}
|
|
if c.refusal != "" {
|
|
send(map[string]any{"jsonrpc": "2.0", "id": get["id"], "error": map[string]any{"code": -32000, "message": c.refusal}})
|
|
} else {
|
|
send(map[string]any{"jsonrpc": "2.0", "id": get["id"], "result": c.result})
|
|
}
|
|
if got := <-answers; got != c.want {
|
|
t.Errorf("%s %s: %q, want %q", c.what, c.key, got, c.want)
|
|
}
|
|
}
|
|
_ = inW.Close()
|
|
}
|