Files
mesh-sdk/go/stdio_test.go
T
jschoubben 5fa64be894 Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3
Main reverted G's half merge (#189, reverted by 8afc3eb in #191) and then took the reader release's S1-S5, which
copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the
release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's
empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's head f30dee54 only by those and
the release's two tests (objects cap option, seat states and stream kinds).

Builds: design 47 G (the milestone's sdk part, as at f30dee54)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 12:49:56 +01:00

416 lines
14 KiB
Go

package stdio
import (
"bufio"
"encoding/json"
"errors"
"io"
"strings"
"sync"
"testing"
"time"
)
// A Go bundle serves as the module the runtime names, answers calls concurrently, and emits through
// the runtime with the TypeScript SDK's envelope (novox/hq ADR 0193).
func TestABundleEmitsThroughTheRuntimeAndServesAsItsModule(t *testing.T) {
t.Setenv(ServedAs, "chime")
t.Setenv("MESH_MODULE", "chime")
t.Setenv("MESH_NODE", "anchor")
// 1887/AC-wake: Served closes once Serve has the runtime's channel, not before; the globals are put back after.
servesAlone(t)
select {
case <-Served():
t.Fatal("Served closed before Serve")
default:
}
inR, inW := io.Pipe()
outR, outW := io.Pipe()
go func() {
_ = ServeOn("", []Tool{{Name: "ring", Description: "rings", Run: func(map[string]any) (any, error) {
if err := Emit("rang", map[string]any{"times": 1}); err != nil {
return nil, err
}
return map[string]any{"rang": true}, 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')) }
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": map[string]any{}})
if got := next()["result"].(map[string]any)["serverInfo"].(map[string]any)["name"]; got != "chime" {
t.Fatalf("served as %v, want the module the runtime named", got)
}
select {
case <-Served():
case <-time.After(2 * time.Second):
t.Fatal("Served did not close once serving")
}
send(map[string]any{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": map[string]any{"name": "ring", "arguments": map[string]any{}}})
asked := next()
if asked["method"] != "mesh/publish" {
t.Fatalf("the tool's emit was not asked of the runtime: %v", asked)
}
p := asked["params"].(map[string]any)
h := p["headers"].(map[string]any)
if p["key"] != "rang" || p["node"] != "anchor" || h["x-source"] != "chime" || h["content-type"] != "application/json" || len(h["x-event-id"].(string)) != 36 {
t.Errorf("the envelope is not the SDK's: %v", p)
}
send(map[string]any{"jsonrpc": "2.0", "id": asked["id"], "result": map[string]any{}})
answered := next()
text := answered["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"]
if answered["id"] != float64(2) || text != `{"rang":true}` {
t.Errorf("the call answered %v", answered)
}
_ = inW.Close()
}
// A Go bundle subscribes and asks through the runtime (novox/hq ADR 0198): the event is answered only
// after the handler took it, a handler's error is answered as one, and a tool call is asked as the module.
func TestABundleSubscribesAndAsksThroughTheRuntime(t *testing.T) {
servesAlone(t)
t.Setenv(ServedAs, "watcher")
inR, inW := io.Pipe()
outR, outW := io.Pipe()
ready := make(chan struct{})
go func() {
_ = ServeOn("", []Tool{{Name: "start", Run: func(map[string]any) (any, error) {
go func() {
_ = Subscribe("database.*", func(e Envelope) error {
if e.Key == "database.bad" {
return errors.New("not taken")
}
return nil
})
close(ready)
}()
answer, err := Ask("postgres.list", map[string]any{})
return json.RawMessage(answer), err
}}}, 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')) }
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "start"}})
// Two asks arrive, in either order: the subscription and the tool call.
asked := map[string]map[string]any{}
for len(asked) < 2 {
m := next()
asked[m["method"].(string)] = m
}
sub, ask := asked["mesh/subscribe"], asked["mesh/ask"]
if sub["params"].(map[string]any)["pattern"] != "database.*" || ask["params"].(map[string]any)["key"] != "postgres.list" {
t.Fatalf("asked %v", asked)
}
send(map[string]any{"jsonrpc": "2.0", "id": sub["id"], "result": map[string]any{}})
send(map[string]any{"jsonrpc": "2.0", "id": ask["id"], "result": []string{"a", "b"}})
if r := next(); r["id"] != float64(1) || r["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"] != `["a","b"]` {
t.Fatalf("the call answered %v", r)
}
<-ready
send(map[string]any{"jsonrpc": "2.0", "id": 7, "method": "mesh/event", "params": map[string]any{"envelope": map[string]any{"key": "database.provisioned", "node": "anchor", "body": map[string]any{}}}})
if r := next(); r["id"] != float64(7) || r["error"] != nil {
t.Errorf("a taken event was answered %v", r)
}
send(map[string]any{"jsonrpc": "2.0", "id": 8, "method": "mesh/event", "params": map[string]any{"envelope": map[string]any{"key": "database.bad", "node": "anchor", "body": map[string]any{}}}})
if r := next(); r["id"] != float64(8) || r["error"] == nil {
t.Errorf("a refused event was answered %v, want an error so the runtime does not acknowledge it", r)
}
_ = inW.Close()
if !TopicMatches("a.#", "a.b.c") || TopicMatches("a.*", "a.b.c") || !TopicMatches("#", "x") {
t.Error("topic matching differs from the TypeScript SDK's")
}
// Design 47 G A-101: the bus's `>` is one or more tokens at the end, and only there.
for pattern, want := range map[string]map[string]bool{
"s.rev.>": {"s.rev.a": true, "s.rev.a.b": true, "s.rev": false, "s.other.a": false},
"s.>.x": {"s.a.x": false},
">": {"a": true, "a.b": true},
} {
for key, ok := range want {
if TopicMatches(pattern, key) != ok {
t.Errorf("TopicMatches(%q, %q) = %v, want %v", pattern, key, !ok, ok)
}
}
}
// Issue 774 notes 84-87: a malformed key is refused before the runtime is asked.
for _, key := range []string{"seat:node-service-manager.stop@novox@g14", "seat:a.*", "postgres.list@", "postgres.list@a.b"} {
if _, err := Ask(key, nil); err == nil || strings.Contains(err.Error(), "Serve") {
t.Errorf("Ask(%q) = %v, want the key refused", key, err)
}
}
}
// A subscribe the runtime refused leaves no handler behind: asked again until the runtime takes it, a
// module's handler is handed each event once, not once per attempt (novox/hq issue 276).
func TestARefusedSubscribeLeavesNoHandlerBehind(t *testing.T) {
servesAlone(t)
t.Setenv(ServedAs, "plex")
inR, inW := io.Pipe()
outR, outW := io.Pipe()
var mu sync.Mutex
handled := 0
subscribed := make(chan error, 2)
beats := make(chan Heartbeat, 2)
go func() {
_ = ServeOn("", []Tool{{Name: "start", Run: func(map[string]any) (any, error) {
go func() {
for i := 0; i < 2; i++ {
if i == 0 {
_ = OnHeartbeat(func(hb Heartbeat) { beats <- hb })
}
err := Subscribe("*.download.completed", func(Envelope) error {
mu.Lock()
handled++
mu.Unlock()
return nil
})
subscribed <- err
if err == nil {
return
}
}
}()
return "started", 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')) }
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "start"}})
answered := 0
for answered < 2 {
m := next()
switch m["method"] {
case "mesh/subscribe":
if answered == 0 {
send(map[string]any{"jsonrpc": "2.0", "id": m["id"], "error": map[string]any{"code": -32000, "message": "not yet"}})
} else {
send(map[string]any{"jsonrpc": "2.0", "id": m["id"], "result": map[string]any{}})
}
answered++
}
}
if err := <-subscribed; err == nil || err.Error() != "not yet" {
t.Fatalf("the refused subscribe said %v", err)
}
if err := <-subscribed; err != nil {
t.Fatalf("the second subscribe said %v", err)
}
send(map[string]any{"jsonrpc": "2.0", "id": 9, "method": "mesh/event", "params": map[string]any{"envelope": map[string]any{"key": "radarr.download.completed", "node": "anchor", "body": map[string]any{}}}})
for {
if r := next(); r["id"] == float64(9) {
if r["error"] != nil {
t.Fatalf("the event was answered %v", r)
}
break
}
}
// The consumer's heartbeat between events reaches the bundle's hook: a notification, and one asked with an id,
// which is answered.
send(map[string]any{"jsonrpc": "2.0", "method": HeartbeatMethod, "params": map[string]any{"at": "2026-11-04T13:00:00Z"}})
send(map[string]any{"jsonrpc": "2.0", "id": 10, "method": HeartbeatMethod, "params": map[string]any{"at": "2026-11-04T13:00:05Z",
"missed": true, "why": "no heartbeat from the bus for 10s"}})
for {
if r := next(); r["id"] == float64(10) {
break
}
}
got := map[bool]Heartbeat{}
for i := 0; i < 2; i++ {
select {
case hb := <-beats:
got[hb.Missed] = hb
case <-time.After(3 * time.Second):
t.Fatal("a heartbeat did not reach the hook")
}
}
if got[false].At.IsZero() || got[true].Why == "" {
t.Errorf("the hook was handed %v", got)
}
_ = inW.Close()
mu.Lock()
defer mu.Unlock()
if handled != 1 {
t.Fatalf("one event was handled %d times", handled)
}
}
// A message the runtime has not read yet does not stop the channel taking its answers: with one tool
// answer blocked on the pipe, a refused subscribe is still delivered to Subscribe, which returns the
// refusal — no lock of the channel is held across a write to the pipe (novox/hq issue 1286/AC1, 1286/AC2).
func TestARefusedSubscribeIsDeliveredWhileAMessageWaitsOnThePipe(t *testing.T) {
servesAlone(t)
t.Setenv(ServedAs, "plex")
inR, inW := io.Pipe()
outR, outW := io.Pipe()
defer func() { _ = inW.Close() }()
go func() {
_ = ServeOn("", []Tool{{Name: "quick", Run: func(map[string]any) (any, error) { return "ok", nil }}}, inR, outW)
}()
subscribed := make(chan error, 1)
go func() {
for {
if _, err := live(); err == nil {
break
}
time.Sleep(time.Millisecond)
}
subscribed <- Subscribe("*.download.completed", func(Envelope) error { return nil })
}()
go func() {
lines := bufio.NewScanner(outR)
if !lines.Scan() {
return
}
var req map[string]any
_ = json.Unmarshal(lines.Bytes(), &req)
send := func(m map[string]any) { b, _ := json.Marshal(m); _, _ = inW.Write(append(b, '\n')) }
// A tool answer the runtime has not read: its write waits on the pipe, and no one reads further.
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "quick"}})
time.Sleep(100 * time.Millisecond)
send(map[string]any{"jsonrpc": "2.0", "id": req["id"], "error": map[string]any{"code": -32000, "message": "not yet"}})
}()
deadline := time.After(2 * time.Second)
select {
case err := <-subscribed:
if err == nil || err.Error() != "not yet" {
t.Fatalf("the refused subscribe said %v", err)
}
case <-deadline:
t.Fatal("Subscribe did not return in 2 s: the channel is stuck on a write to a pipe nobody reads")
}
// Let the waiting answer go, so nothing is left blocked.
go func() { _, _ = io.Copy(io.Discard, outR) }()
}
// servesAlone gives a test that serves its own serving state, put back when it ends: no runtime's channel yet and a
// Served not yet closed. ServeOn sets both for the whole process, so without it a test saw the channel and the closed
// Served an earlier test left behind (the check of release sdk1, 2026-11-02: tests leaking package state).
func servesAlone(t *testing.T) {
t.Helper()
currentMu.Lock()
oldCurrent := current
current = nil
currentMu.Unlock()
oldServed := served
served, servedOnce = make(chan struct{}), sync.Once{}
t.Cleanup(func() {
currentMu.Lock()
current = oldCurrent
currentMu.Unlock()
served, servedOnce = oldServed, sync.Once{}
select {
case <-oldServed: // it was closed: its Once is spent too
servedOnce.Do(func() {})
default:
}
})
}
// Design 47 G §559: a bundle's acknowledged publish asks `ack: true` and fails on the runtime's `{error}`; its
// subscription for new messages asks `deliver: new`.
func subABundleAsksAnAcknowledgedPublishAndASubscriptionForNewMessages(t *testing.T) {
servesAlone(t)
t.Setenv(ServedAs, "keeper")
inR, inW := io.Pipe()
outR, outW := io.Pipe()
errs := make(chan error, 2)
go func() {
_ = ServeOn("", []Tool{{Name: "go", Run: func(map[string]any) (any, error) {
go func() { errs <- SubscribeNew("mesh-conditions.findings-asked", func(Envelope) error { return nil }) }()
errs <- Emit("finding-written", map[string]any{}, EmitOptions{Ack: true})
return map[string]any{}, 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')) }
send(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": map[string]any{"name": "go"}})
asked := map[string]map[string]any{}
for len(asked) < 2 {
m := next()
asked[m["method"].(string)] = m
}
sub, pub := asked["mesh/subscribe"]["params"].(map[string]any), asked["mesh/publish"]["params"].(map[string]any)
if sub["deliver"] != "new" || pub["ack"] != true {
t.Fatalf("asked subscribe %v, publish %v", sub, pub)
}
send(map[string]any{"jsonrpc": "2.0", "id": asked["mesh/subscribe"]["id"], "result": map[string]any{}})
send(map[string]any{"jsonrpc": "2.0", "id": asked["mesh/publish"]["id"], "result": map[string]any{"error": "no stream"}})
var failed bool
for range 2 {
if err := <-errs; err != nil {
failed = failed || strings.Contains(err.Error(), "did not acknowledge finding-written: no stream")
}
}
if !failed {
t.Errorf("an {error} answer to an acknowledged publish was not returned as an error")
}
_ = inW.Close()
}