views: an ask through a tool reads the tool runner's {result, error} envelope

A5's request: the bus module's keeper asks the indexer's first view through the tool runner, which answers in that
envelope; its error is the ask's.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 12:51:56 +01:00
parent 0b25bce239
commit fc50486190
2 changed files with 30 additions and 1 deletions
+12 -1
View File
@@ -352,7 +352,8 @@ func (f *Follower) Gap(ctx context.Context, why string) error {
return f.askUntil(ctx, "a gap: "+why, false)
}
// AskVia is a Follower's Ask through a process's call of the owner's tool (process.Conn.Call), decoding its answer.
// AskVia is a Follower's Ask through a call of the owner's tool (process.Conn.Call, or stdio.Ask through the tool
// runner), decoding its answer: bare, or in the tool runner's envelope {result, error}, whose error is the ask's.
func AskVia(call func(key string, body any) (json.RawMessage, error), tool string) func(context.Context, string) (Answer, error) {
return func(_ context.Context, after string) (Answer, error) {
body := map[string]any{}
@@ -363,6 +364,16 @@ func AskVia(call func(key string, body any) (json.RawMessage, error), tool strin
if err != nil {
return Answer{}, err
}
var wrapped struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
if json.Unmarshal(raw, &wrapped) == nil && (len(wrapped.Result) > 0 || wrapped.Error != "") {
if wrapped.Error != "" {
return Answer{}, fmt.Errorf("%s answered: %s", tool, wrapped.Error)
}
raw = wrapped.Result
}
return DecodeAnswer(raw)
}
}
+18
View File
@@ -260,6 +260,24 @@ func TestViews(t *testing.T) {
t.Errorf("asked again by hand: %v, %v", again, err)
}
})
t.Run("an ask through a tool reads the answer bare or in the tool runner's envelope, and its error", func(t *testing.T) {
bare := `{"v":1,"owner":"o","state":"s","revision":3,"values":{"k":"1"}}`
for name, c := range map[string]struct {
answer string
rev uint64
err string
}{
"bare": {bare, 3, ""},
"in an envelope": {`{"result":` + bare + `}`, 3, ""},
"an error": {`{"error":"not yet"}`, 0, "not yet"},
} {
ask := AskVia(func(string, any) (json.RawMessage, error) { return json.RawMessage(c.answer), nil }, "o.first_view")
a, err := ask(context.Background(), "")
if (c.err == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.err)) || a.Revision != c.rev {
t.Errorf("%s: %+v, %v", name, a, err)
}
}
})
t.Run("an event names its state on its subject, and decodes as it was encoded", func(t *testing.T) {
if s := StateOfEvent("mesh.stream.mesh-delivery-indexer.event.views.config-candidates"); s != "config-candidates" {
t.Errorf("state of subject = %q", s)