The Go SDK subscribes and asks through the runtime (hq ADR 0198)
Subscribe(pattern, handler) asks the runtime for the module's events and answers each mesh/event only after the matching handlers returned nil, so the runtime acknowledges it on the bus then and not before; Ask(key, body) calls a tool as the module. Topic matching is the TypeScript SDK's.
This commit is contained in:
@@ -3,6 +3,7 @@ package stdio
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -66,3 +67,75 @@ func TestABundleEmitsThroughTheRuntimeAndServesAsItsModule(t *testing.T) {
|
||||
}
|
||||
_ = 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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user