diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 15bac45..cf7ed7e 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -1170,7 +1170,11 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any } return failure(why) } - content := []map[string]any{{"type": "text", "text": pretty(got.Result)}} + result, unread := resolveSent(s.conn, got.Result) + content := []map[string]any{{"type": "text", "text": pretty(result)}} + if unread != "" { + content = append(content, map[string]any{"type": "text", "text": unread}) + } if got.Node != "" { content = append(content, map[string]any{"type": "text", "text": "answered by " + got.Node}) } diff --git a/node-tools/internal/console/sent.go b/node-tools/internal/console/sent.go new file mode 100644 index 0000000..6276914 --- /dev/null +++ b/node-tools/internal/console/sent.go @@ -0,0 +1,31 @@ +package console + +import ( + "encoding/json" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/buscalls" + "git.novox.be/novox/mesh-sdk/go/stateobjects" + "github.com/novox/mesh-tools/node-tools/internal/bus" +) + +// resolveSent is the answer a person reads (ADR 0666 §16): an answer a module sent as a reference to an object of its +// sent store is read from it when the bus grants this runner's account the read (the standing grant the manifests give +// the callers of the verb), else shown as its reference with its size and why it was not read. +func resolveSent(conn *bus.Conn, result json.RawMessage) (json.RawMessage, string) { + ref, ok := stateobjects.ReadRef(result) + if !ok { + return result, "" + } + whole, err := stateobjects.Receive(result, func(r stateobjects.Ref) ([]byte, error) { + got, err := conn.SentGet(buscalls.ObjectsSentGetRequest{Store: r.Store, Object: r.Object, Size: r.Size, Digest: r.Digest}) + if err != nil { + return nil, err + } + return got.Data, nil + }) + if err != nil { + return result, fmt.Sprintf("the answer is %d bytes, held as the object %s of %s, and was not read: %v", ref.Size, ref.Object, ref.Store, err) + } + return whole, "" +} diff --git a/node-tools/internal/console/sent_test.go b/node-tools/internal/console/sent_test.go new file mode 100644 index 0000000..bd1d0c9 --- /dev/null +++ b/node-tools/internal/console/sent_test.go @@ -0,0 +1,56 @@ +package console + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/stateobjects" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + "github.com/novox/mesh-tools/node-tools/internal/bus" + "github.com/novox/mesh-tools/node-tools/internal/meshtest" +) + +// ADR 0666 §16: an answer a module sent as a reference is read from its sent store for the person asking; one the bus does +// not let the runner read is shown as its reference with its size and why it was not read. +func TestAnAnswerSentAsAReferenceIsReadForThePersonAsking(t *testing.T) { + nc, err := nats.Connect(meshtest.URL(t)) + if err != nil { + t.Fatal(err) + } + defer nc.Close() + js, _ := jetstream.New(nc) + obs, err := js.CreateObjectStore(context.Background(), jetstream.ObjectStoreConfig{Bucket: "m_sent-objects"}) + if err != nil { + t.Fatal(err) + } + conn, err := bus.Connect(bus.Credential{URL: meshtest.URL(t), Module: "console", Node: "anchor"}) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + body := []byte(`{"lines":"` + strings.Repeat("x", 3<<20) + `"}`) + sum := sha256.Sum256(body) + ref := stateobjects.Ref{Store: "m_sent-objects", Object: "tool.lines." + hex.EncodeToString(sum[:]), Size: int64(len(body)), + Digest: hex.EncodeToString(sum[:]), Form: stateobjects.FormVersion} + if _, err := obs.PutBytes(context.Background(), ref.Object, body); err != nil { + t.Fatal(err) + } + if got, unread := resolveSent(conn, stateobjects.Wrap(ref)); unread != "" || string(got) != string(body) { + t.Fatalf("read %d bytes of %d, %q", len(got), len(body), unread) + } + missing := ref + missing.Store = "gone_sent-objects" + wrapped := stateobjects.Wrap(missing) + got, unread := resolveSent(conn, wrapped) + if string(got) != string(wrapped) || !strings.Contains(unread, "3145") || !strings.Contains(unread, "gone_sent-objects") { + t.Fatalf("an unread answer showed %q and %q", got, unread) + } + if got, unread := resolveSent(conn, []byte(`{"a":1}`)); unread != "" || string(got) != `{"a":1}` { + t.Fatalf("a plain answer showed %q, %q", got, unread) + } +}