package launch import ( "encoding/json" "os" "path/filepath" "slices" "strings" "sync" "testing" "time" ) // A bundle that asks for one of its values and watches it, and writes each change it is handed to $OUT. const provisionBundle = `#!/bin/sh while IFS= read -r line; do id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\)[,}].*/\1/p') case "$line" in *'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;; *'"method":"tools/list"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" printf '{"jsonrpc":"2.0","id":"g1","method":"mesh/provision.get","params":{"kind":"config","key":"role"}}\n' printf '{"jsonrpc":"2.0","id":"w1","method":"mesh/provision.watch","params":{"kind":"config","key":"role"}}\n' ;; *'"method":"mesh/provision"'*) printf '%s\n' "$line" >> "$OUT" printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;; *'"id":"g1"'*) printf '%s\n' "$line" >> "$OUT" ;; esac done ` type provisioning struct { subscribing mu sync.Mutex asked []string } func (b *provisioning) Provision(verb string, params json.RawMessage) (json.RawMessage, error) { b.mu.Lock() defer b.mu.Unlock() b.asked = append(b.asked, verb+" "+string(params)) return json.RawMessage(`"the build machine"`), nil } func (b *provisioning) ProvisionWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) { b.mu.Lock() b.asked = append(b.asked, "watch "+string(params)) b.mu.Unlock() return func() {}, deliver(json.RawMessage(`{"kind":"config","key":"role","value":"the build machine"}`)) } // novox/hq ADR 0349 §14, ADR 0370 §4: a bundle's ask for its values reaches the runtime's bus from its own channel, // answered there, and a watch's change is handed to it naming the watch it asked. func TestABundleAsksForItsValuesAndIsHandedItsWatchsChanges(t *testing.T) { entry := filepath.Join(t.TempDir(), "bundle") if err := os.WriteFile(entry, []byte(provisionBundle), 0o755); err != nil { t.Fatal(err) } out := filepath.Join(t.TempDir(), "out") b := &provisioning{} l, err := Start("claude-code", entry, append(os.Environ(), "OUT="+out), b, t.Logf) if err != nil { t.Fatal(err) } t.Cleanup(l.Stop) deadline := time.Now().Add(10 * time.Second) for { said, _ := os.ReadFile(out) if strings.Count(string(said), "\n") >= 2 { // Each line as the bundle read it, by what it is: the get's answer, and the watch's change. got := map[string]string{} for _, line := range strings.Split(strings.TrimSpace(string(said)), "\n") { var m struct { ID any `json:"id"` Method string `json:"method"` Result json.RawMessage `json:"result"` Params json.RawMessage `json:"params"` } if err := json.Unmarshal([]byte(line), &m); err != nil { t.Fatalf("the bundle read %s", line) } if m.Method != "" { got[m.Method] = string(m.Params) } else { got["answer"] = string(m.Result) } } if got["answer"] != `"the build machine"` { t.Errorf("the get was answered %s", got["answer"]) } if got["mesh/provision"] != `{"change":{"kind":"config","key":"role","value":"the build machine"},"watch":"w1"}` { t.Errorf("the watch was handed %s", got["mesh/provision"]) } break } if time.Now().After(deadline) { t.Fatalf("the bundle was handed %q", said) } time.Sleep(20 * time.Millisecond) } b.mu.Lock() defer b.mu.Unlock() slices.Sort(b.asked) if len(b.asked) != 2 || b.asked[0] != `get {"kind":"config","key":"role"}` || b.asked[1] != `watch {"kind":"config","key":"role"}` { t.Errorf("the runtime's bus was asked %q", b.asked) } }