package stdio import ( "bufio" "encoding/json" "io" "strings" "testing" "time" ) // A Go bundle keeps and watches its state through the runtime (novox/hq ADR 0201): the watch's // current values are handled before it returns, its changes are named by the watch's id, a change for // no watch of this bundle is refused, and put, get and keys are asked of the runtime as the module. func TestABundleKeepsAndWatchesStateThroughTheRuntime(t *testing.T) { t.Setenv(ServedAs, "keeper") inR, inW := io.Pipe() outR, outW := io.Pipe() seen := make(chan string, 16) watched := make(chan error, 1) go func() { _ = ServeOn("", []Tool{ {Name: "start", Run: func(map[string]any) (any, error) { go func() { watched <- State("servers").Watch("", func(c StateChange) error { seen <- c.Op + " " + c.Key + " " + string(c.Value) return nil }) }() return "started", nil }}, {Name: "remember", Run: func(a map[string]any) (any, error) { return State("servers").Put(a["key"].(string), a["value"]) }}, {Name: "recall", Run: func(a map[string]any) (any, error) { return State("servers").Get(a["key"].(string)) }}, }, 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"}}) got := map[string]map[string]any{} for len(got) < 2 { m := next() if m["method"] != nil { got[m["method"].(string)] = m } else { got["answer"] = m } } watch := got["mesh/state.watch"] if p := watch["params"].(map[string]any); p["state"] != "servers" || p["key"] != "" { t.Fatalf("asked to watch %v", p) } // The current value, for this watch, before the watch is answered. send(map[string]any{"jsonrpc": "2.0", "id": 50, "method": "mesh/state", "params": map[string]any{"watch": watch["id"], "change": map[string]any{"state": "servers", "key": "all.one", "op": "put", "value": map[string]any{"url": "http://one"}, "current": true}}}) if r := next(); r["id"] != float64(50) || r["error"] != nil { t.Fatalf("a current value was answered %v", r) } if s := <-seen; s != `put all.one {"url":"http://one"}` { 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 for a watch this bundle never asked for is refused. send(map[string]any{"jsonrpc": "2.0", "id": 51, "method": "mesh/state", "params": map[string]any{"watch": "bundle-999", "change": map[string]any{"key": "x", "op": "put"}}}) if r := next(); r["id"] != float64(51) || !strings.Contains(r["error"].(map[string]any)["message"].(string), "names no watch") { t.Fatalf("a change for no watch was answered %v", r) } // Put: asked of the runtime as the module, the revision is the tool's answer. send(map[string]any{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": map[string]any{"name": "remember", "arguments": map[string]any{"key": "all.two", "value": map[string]any{"url": "http://two"}}}}) put := next() if put["method"] != "mesh/state.put" || put["params"].(map[string]any)["key"] != "all.two" || put["params"].(map[string]any)["state"] != "servers" { t.Fatalf("asked %v", put) } send(map[string]any{"jsonrpc": "2.0", "id": put["id"], "result": map[string]any{"revision": 9}}) if r := next(); r["id"] != float64(2) || r["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"] != "9" { t.Fatalf("the put answered %v", r) } // Get of a key with no value is nil. send(map[string]any{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": map[string]any{"name": "recall", "arguments": map[string]any{"key": "nothing"}}}) get := next() send(map[string]any{"jsonrpc": "2.0", "id": get["id"], "result": nil}) if r := next(); r["id"] != float64(3) || r["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"] != "null" { t.Fatalf("the get answered %v", r) } _ = inW.Close() }