package console import ( "context" "crypto/sha256" "encoding/hex" "errors" "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" ) // A refusal is said only for the bus's own refusals; a tool's answer that mentions authorization is the // tool's answer, not the account's. func TestARefusalIsSaidOnlyForTheBussOwn(t *testing.T) { for msg, refusal := range map[string]bool{ `nats: Permissions Violation for Publish to "mesh.mod.x.tool.y"`: true, "nats: Authorization Violation": true, `claude-code's servers.all.x carries a field "Authorization", which names a credential`: false, } { got := strings.HasPrefix(whyItFailed("x.y", errors.New(msg)), "this account may not call") if got != refusal { t.Errorf("%q read as a refusal: %v, want %v", msg, got, refusal) } } // 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. t.Run("AnAnswerSentAsAReferenceIsReadForThePersonAsking", func(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", 9<<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, "9437") || !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) } }) } // **A timeout says it is not a failure** (novox/hq issue 265): a push that outlasted the wait had // pushed, and the caller read "did not answer in time". No hint names a seat that is gone (novox/hq issue 1126): // the grammar's example, the handshake's instructions and a timeout's remedy named the controller's verbs. func TestATimeoutIsNotAFailure(t *testing.T) { got := whyItFailed("seat:mesh-delivery.deliveries", errors.New("timeout")) for _, want := range []string{"not a failure", "may already have done"} { if !strings.Contains(got, want) { t.Errorf("a seat's timeout does not say %q: %s", want, got) } } for name, text := range map[string]string{ "grammar": grammar, "flat instructions": (&Surface{Flat: true, who: "x"}).instructions(), "deep instructions": (&Surface{who: "x"}).instructions(), "timeout": got, } { if strings.Contains(text, "controller") { t.Errorf("%s names the controller: %s", name, text) } // The seats were renamed (ADR 0634 §9): no hint names the seat that placed modules or enrolled a machine by its old name. for _, old := range []string{"mesh-placement", "mesh-enrolment"} { if strings.Contains(text, old) { t.Errorf("%s names the seat %s, which is now mesh-module-assigner or mesh-node-enroller: %s", name, old, text) } } } }