mesh/merge-gate head eaec209a371b merged into trunk 9ebfb87b5d0c — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head eaec209a371b merged into trunk 9ebfb87b5d0c — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
278 lines
9.7 KiB
Go
278 lines
9.7 KiB
Go
package stdio
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"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")
|
|
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)
|
|
}
|
|
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) {
|
|
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")
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
t.Setenv(ServedAs, "plex")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
var mu sync.Mutex
|
|
handled := 0
|
|
subscribed := make(chan error, 2)
|
|
go func() {
|
|
_ = ServeOn("", []Tool{{Name: "start", Run: func(map[string]any) (any, error) {
|
|
go func() {
|
|
for i := 0; i < 2; i++ {
|
|
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
|
|
}
|
|
}
|
|
_ = 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) {
|
|
t.Setenv(ServedAs, "plex")
|
|
inR, inW := io.Pipe()
|
|
outR, outW := io.Pipe()
|
|
defer func() { _ = inW.Close() }()
|
|
// Not the channel an earlier run left behind: Subscribe goes to the one this run serves.
|
|
currentMu.Lock()
|
|
current = nil
|
|
currentMu.Unlock()
|
|
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) }()
|
|
}
|