mesh/merge-gate head 1bc5719b8b85 merged into trunk a2d2af00dc5d beside mesh-host#116@bff1ac14 — pass, with a note: builds mesh-sdk (+40 dependent(s)) → ac…
mesh/repo-check head 1bc5719b8b85 merged into trunk a2d2af00dc5d beside mesh-host#116@bff1ac14 — pass: its merge-check.sh passed, each part in its toolchai…
mesh/delivery delivered
mesh/delivery-group group release/57-config-and-provisioning delivered: every member is delivered
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()
|
|
}
|