E-3: mesh_wait takes any event name (issue 1966)
waitAnswer no longer reads the membership: each name becomes its subjects through buscompose.EventSubjects, optionally narrowed by from (seat, module or stream), at most 16 names, a subject once; the heard event is named by buscompose.EventName with its namespace and its own time, and a decided event says the operator's answer is read only with claude_code_answer. WaitIssued and Membership.Waits go; the refusals are one table. Builds: design 34 every-event §13, §15, §17, §19, §26 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -75,9 +75,6 @@ type Membership struct {
|
||||
// Hears are the module streams the module hears, each through a durable consumer on that stream the
|
||||
// runtime binds beside the one on EVENTS (ADR 0363 §8).
|
||||
Hears []HeardIssued `json:"hears,omitempty"`
|
||||
// Waits are the event families a session may wait on through the mesh MCP server (novox/hq ADR 0367 §2):
|
||||
// the tool runner's alone, composed from its manifest with the grant that lets it subscribe them.
|
||||
Waits []WaitIssued `json:"waits,omitempty"`
|
||||
// Bindings are the module's bus state in the new form, by local name (novox/hq ADR 0595 §1, mesh-sdk
|
||||
// membership.BusBinding): its object stores, streams, work queues and ledgers, its own alone (bindings.go).
|
||||
Bindings []membership.BusBinding `json:"bindings,omitempty"`
|
||||
|
||||
@@ -17,13 +17,6 @@ import (
|
||||
// subscribes the subjects when called and unsubscribes them before it answers, so an event published
|
||||
// between two calls is not heard — a session reads the state from its owner after every wake (§4).
|
||||
|
||||
// WaitIssued is one event family the mesh issued this account to wait on (ADR 0367 §2): the name a session
|
||||
// asks for, and the subject it lands on. Only the tool runner's membership carries any.
|
||||
type WaitIssued struct {
|
||||
Event string `json:"event"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
// Heard is one event a wait heard: the subject it arrived on, its body, when it arrived, and when it was published, the
|
||||
// event's own header `x-time` as its emitter wrote it, empty for an event that carries none (design 34 every-event §12).
|
||||
type Heard struct {
|
||||
|
||||
@@ -2,9 +2,9 @@ package console
|
||||
|
||||
// mesh_wait: a session waits on the bus for one event instead of polling (novox/hq ADR 0367).
|
||||
//
|
||||
// **Only the families the tool runner was issued** (§2, §6): its membership names them, composed by the
|
||||
// mesh from the tool runner's manifest together with the grant that lets this account subscribe them.
|
||||
// A name not listed there is refused here, before the bus would refuse it; no argument or setting adds one.
|
||||
// **Any event, by its name** (novox/hq ADR 0670 §1, ADR 0671 §1; design 34 every-event §13, §26): the grants part
|
||||
// grants this machine's tool runner every event subject, a seat's, a module's and a module stream's, and a name
|
||||
// becomes its subjects through mesh-sdk's buscompose.EventSubjects; no membership and no manifest names a list.
|
||||
// **An event only wakes the session** (§4): what it says is read again from the verb that owns it.
|
||||
|
||||
import (
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
)
|
||||
|
||||
// WaitDefault is how long a wait lasts when no timeout is given, and WaitLongest the most it may last
|
||||
@@ -25,20 +27,29 @@ var (
|
||||
WaitLongest = 10 * time.Minute
|
||||
)
|
||||
|
||||
// waitMostNames is the most event names one call gives (design 34 every-event §13, §26): each name is six
|
||||
// subscriptions for the call, so 16 names are 96, far above what a watcher names.
|
||||
const waitMostNames = 16
|
||||
|
||||
func waitTool() map[string]any {
|
||||
return map[string]any{"name": verbWait, "inputSchema": map[string]any{"type": "object", "properties": map[string]any{
|
||||
"subjects": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
||||
"description": "one or more event families to wait on, by name, e.g. `mesh-knowledge.reviewed`; only " +
|
||||
"those the mesh granted this machine's tool runner — a refusal names them"},
|
||||
"description": fmt.Sprintf("one or more event names, `<seat or module>.<event>`, such as "+
|
||||
"`mesh-delivery-view.delivered`, at most %d; an event whose subject goes on after the name (an asker, a "+
|
||||
"kind, a module) is heard by the name too", waitMostNames)},
|
||||
"from": map[string]any{"type": "string", "enum": []string{"seat", "module", "stream"},
|
||||
"description": "narrows every name to a seat's events, a module's or a module stream's, for an emitter " +
|
||||
"whose name is more than one of them (optional)"},
|
||||
"filter": map[string]any{"type": "object",
|
||||
"description": "fields of the event's body that must equal the given values, e.g. {\"kind\": \"decision\", \"number\": 367} (optional)"},
|
||||
"description": "fields of the event's body that must equal the given values, e.g. {\"kind\": \"decision\", \"number\": 367}: the filter narrows by the body's fields (optional)"},
|
||||
"timeout": map[string]any{"type": "number",
|
||||
"description": fmt.Sprintf("seconds to wait: %d when absent, %d at most", int(WaitDefault.Seconds()), int(WaitLongest.Seconds()))},
|
||||
}, "required": []string{"subjects"}},
|
||||
"description": "Wait for the first event of the named families that matches the filter, and answer it with its " +
|
||||
"family, body and time — or say it timed out. Call it in a loop from a background command, and after every " +
|
||||
"answer read the state from the verb that owns it: an event only wakes you, and one published between two " +
|
||||
"waits is not heard (novox/hq ADR 0367)."}
|
||||
"description": "Wait for the first event of the named events that matches the filter, and answer it with its " +
|
||||
"name, where it came from, body and times — or say it timed out. Call it in a loop from a background command, " +
|
||||
"and after every answer read the state from the verb that owns it: an event only wakes you, and one " +
|
||||
"published between two waits is not heard (novox/hq ADR 0367). An operator's answer is read only with " +
|
||||
"claude_code_answer, never from the event."}
|
||||
}
|
||||
|
||||
// waitAnswer answers one mesh_wait call.
|
||||
@@ -56,34 +67,25 @@ func (s *Surface) waitAnswer(ctx context.Context, args map[string]any) map[strin
|
||||
return failure("mesh_wait's filter is an object of body fields to the values they must equal")
|
||||
}
|
||||
|
||||
issued := map[string]string{} // name → subject
|
||||
if m := s.conn.Membership(s.conn.Module()); m != nil {
|
||||
for _, w := range m.Waits {
|
||||
issued[w.Event] = w.Subject
|
||||
}
|
||||
from, _ := args["from"].(string)
|
||||
if args["from"] != nil && from != "seat" && from != "module" && from != "stream" {
|
||||
return failure(fmt.Sprintf("mesh_wait's from is seat, module or stream, not %v", args["from"]))
|
||||
}
|
||||
if len(issued) == 0 {
|
||||
return failure("this machine's tool runner was issued no event families to wait on: the mesh grants them from " +
|
||||
"the tool runner's manifest (novox/hq ADR 0367 §2), and its membership names none yet")
|
||||
if len(names) > waitMostNames {
|
||||
return failure(fmt.Sprintf("mesh_wait names at most %d events in one call, not %d: each name is six "+
|
||||
"subscriptions for the call (design 34 every-event §13)", waitMostNames, len(names)))
|
||||
}
|
||||
byName := make([]string, 0, len(issued))
|
||||
for name := range issued {
|
||||
byName = append(byName, name)
|
||||
}
|
||||
sort.Strings(byName)
|
||||
family := map[string]string{} // subject → name
|
||||
var subjects []string
|
||||
for _, name := range names {
|
||||
subject, granted := issued[name]
|
||||
if !granted {
|
||||
return failure(fmt.Sprintf("mesh_wait may not wait on %q: this machine's tool runner is granted %s alone. "+
|
||||
"A family is added only by a reviewed change to the tool runner's manifest (node-tools' module.json, "+
|
||||
"novox/hq ADR 0367 §6); no argument, call or setting adds one", name, strings.Join(byName, ", ")))
|
||||
subs, err := buscompose.EventSubjects(name, from)
|
||||
if err != nil {
|
||||
return failure("mesh_wait may not wait on it: " + err.Error())
|
||||
}
|
||||
if _, twice := family[subject]; !twice {
|
||||
subjects = append(subjects, subject)
|
||||
for _, subject := range subs {
|
||||
if !slices.Contains(subjects, subject) {
|
||||
subjects = append(subjects, subject)
|
||||
}
|
||||
}
|
||||
family[subject] = name
|
||||
}
|
||||
|
||||
heard, ok, refused, err := s.conn.HearFirst(ctx, subjects, timeout, func(body json.RawMessage) bool { return bodyMatches(body, filter) })
|
||||
@@ -95,10 +97,17 @@ func (s *Surface) waitAnswer(ctx context.Context, args map[string]any) map[strin
|
||||
answer = map[string]any{"timed-out": true, "waited": timeout.String(), "subjects": names,
|
||||
"read": "nothing matching was heard; read the state once from the verb that owns it, then wait again"}
|
||||
} else {
|
||||
answer = map[string]any{
|
||||
"heard": map[string]any{"subject": family[heard.Subject], "body": heard.Body, "at": heard.At.Format(time.RFC3339Nano)},
|
||||
"read": "an event only wakes you: read the state from the verb that owns it before acting (novox/hq ADR 0367 §4)",
|
||||
name, from, _ := buscompose.EventName(heard.Subject)
|
||||
h := map[string]any{"subject": name, "from": from, "body": heard.Body, "at": heard.At.Format(time.RFC3339Nano)}
|
||||
if heard.Published != "" {
|
||||
h["published"] = heard.Published
|
||||
}
|
||||
read := "an event only wakes you: read the state from the verb that owns it before acting (novox/hq ADR 0367 §4)"
|
||||
if strings.HasPrefix(name, "operator-channel.decided") {
|
||||
read = "an event only wakes you: the operator's answer is read only with claude_code_answer, never from " +
|
||||
"the event's body (novox/hq ADR 0670 §5)"
|
||||
}
|
||||
answer = map[string]any{"heard": h, "read": read}
|
||||
}
|
||||
// A subscription the bus refused while others stood is said (design 34 every-event §16).
|
||||
if len(refused) > 0 {
|
||||
@@ -107,7 +116,7 @@ func (s *Surface) waitAnswer(ctx context.Context, args map[string]any) map[strin
|
||||
return answerText(answer)
|
||||
}
|
||||
|
||||
// waitNames is the families a call names: a list of names, or one name.
|
||||
// waitNames is the event names a call gives: a list of names, or one name.
|
||||
func waitNames(given any) ([]string, error) {
|
||||
var names []string
|
||||
switch v := given.(type) {
|
||||
@@ -117,7 +126,7 @@ func waitNames(given any) ([]string, error) {
|
||||
for _, x := range v {
|
||||
name, ok := x.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("mesh_wait's subjects are event names, such as mesh-knowledge.reviewed; %v is not one", x)
|
||||
return nil, fmt.Errorf("mesh_wait's subjects are event names, such as mesh-delivery-view.delivered; %v is not one", x)
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
@@ -126,7 +135,7 @@ func waitNames(given any) ([]string, error) {
|
||||
names[i] = strings.TrimSpace(names[i])
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil, fmt.Errorf("mesh_wait names at least one event family in `subjects`, such as mesh-knowledge.reviewed")
|
||||
return nil, fmt.Errorf("mesh_wait names at least one event in `subjects`, such as mesh-delivery-view.delivered")
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package console
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -11,24 +12,22 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
|
||||
// waitingRuntime is the tool runner on a machine, issued the families ADR 0367 §5 names, as the controller
|
||||
// composes them from its manifest: the surface over its connection, and the mesh to publish on.
|
||||
// waitingRuntime is the tool runner on a machine, issued no waits (novox/hq ADR 0670 §2): the surface over its
|
||||
// connection, and the mesh to publish on.
|
||||
func waitingRuntime(t *testing.T) (*Surface, *mt.Mesh) {
|
||||
t.Helper()
|
||||
mesh := mt.New(t)
|
||||
m := mt.MembershipOf("node-tools", "desk", false, nil)
|
||||
m.Waits = []bus.WaitIssued{
|
||||
{Event: "mesh-knowledge.reviewed", Subject: "mesh.mod.mesh-knowledge.event.reviewed"},
|
||||
{Event: "operator-channel.decided.claude-code", Subject: "mesh.seat.operator-channel.event.decided.claude-code"},
|
||||
}
|
||||
mesh.Issue(t, m)
|
||||
mesh.Issue(t, mt.MembershipOf("node-tools", "desk", false, nil))
|
||||
conn := connect(t, "node-tools", "desk")
|
||||
mt.Until(t, func() error {
|
||||
if got := conn.Membership("node-tools"); got == nil || len(got.Waits) == 0 {
|
||||
if conn.Membership("node-tools") == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return nil
|
||||
@@ -36,6 +35,17 @@ func waitingRuntime(t *testing.T) (*Surface, *mt.Mesh) {
|
||||
return NewSurface(conn, busSource{conn}, "desk.node-tools"), mesh
|
||||
}
|
||||
|
||||
// listening waits until the surface's connection holds n more subscriptions than before: a wait has subscribed.
|
||||
func listening(t *testing.T, s *Surface, before, n int) {
|
||||
t.Helper()
|
||||
mt.Until(t, func() error {
|
||||
if s.conn.Subscriptions() < before+n {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// callWait is one tools/call of mesh_wait, answered as an agent host reads it.
|
||||
func callWait(s *Surface, args map[string]any) (text string, isError bool) {
|
||||
raw, _ := json.Marshal(map[string]any{"name": verbWait, "arguments": args})
|
||||
@@ -50,8 +60,9 @@ func callWait(s *Surface, args map[string]any) (text string, isError bool) {
|
||||
return strings.Join(parts, "\n"), isErr
|
||||
}
|
||||
|
||||
// novox/hq ADR 0367 §1: mesh_wait answers the first event that matches — by its family and its filter — with
|
||||
// its subject, body and time, within a second of its publication, and ignores the others.
|
||||
// novox/hq ADR 0367 §1, ADR 0670 §1 (design 34 every-event §13, §15, §19): mesh_wait answers the first event that
|
||||
// matches — by its name and its filter — with no membership naming it: its whole name, the namespace it arrived on, its
|
||||
// body, when it arrived and when it was published, within a second of its publication, and ignores the others.
|
||||
func TestMeshWaitAnswersTheFirstMatchingEvent(t *testing.T) {
|
||||
s, mesh := waitingRuntime(t)
|
||||
type answer struct {
|
||||
@@ -59,18 +70,39 @@ func TestMeshWaitAnswersTheFirstMatchingEvent(t *testing.T) {
|
||||
isErr bool
|
||||
at time.Time
|
||||
}
|
||||
type heardAnswer struct {
|
||||
Heard struct {
|
||||
Subject string `json:"subject"`
|
||||
From string `json:"from"`
|
||||
Body map[string]any `json:"body"`
|
||||
At time.Time `json:"at"`
|
||||
Published *string `json:"published"`
|
||||
} `json:"heard"`
|
||||
Read string `json:"read"`
|
||||
}
|
||||
nc, err := nats.Connect(mt.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
done := make(chan answer, 1)
|
||||
before := s.conn.Subscriptions()
|
||||
go func() {
|
||||
text, isErr := callWait(s, map[string]any{"subjects": []any{"mesh-knowledge.reviewed"},
|
||||
"filter": map[string]any{"kind": "decision", "number": 367}, "timeout": 20})
|
||||
text, isErr := callWait(s, map[string]any{"subjects": []any{"mesh-delivery-view.delivered"},
|
||||
"filter": map[string]any{"module": "netcheck"}, "timeout": 20})
|
||||
done <- answer{text, isErr, time.Now()}
|
||||
}()
|
||||
// Once it listens: another record's verdict, another family's event, then the one it waits for.
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
mesh.Emit(t, "mesh.mod.mesh-knowledge.event.reviewed", map[string]any{"kind": "decision", "number": 366, "what": "approved"})
|
||||
mesh.Emit(t, "mesh.mod.mesh-knowledge.event.proposed", map[string]any{"kind": "decision", "number": 367})
|
||||
// Once it listens: another module's delivery, another event, then the one it waits for, with its own time.
|
||||
listening(t, s, before, 6)
|
||||
mesh.Emit(t, "mesh.mod.mesh-delivery-view.event.delivered", map[string]any{"module": "grafana"})
|
||||
mesh.Emit(t, "mesh.mod.mesh-delivery-view.event.planned", map[string]any{"module": "netcheck"})
|
||||
msg := nats.NewMsg("mesh.mod.mesh-delivery-view.event.delivered")
|
||||
msg.Header.Set("x-time", "2026-11-12T03:00:00.5Z")
|
||||
msg.Data = []byte(`{"module":"netcheck"}`)
|
||||
published := time.Now()
|
||||
mesh.Emit(t, "mesh.mod.mesh-knowledge.event.reviewed", map[string]any{"kind": "decision", "number": 367, "what": "approved"})
|
||||
if err := nc.PublishMsg(msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got answer
|
||||
select {
|
||||
case got = <-done:
|
||||
@@ -83,32 +115,32 @@ func TestMeshWaitAnswersTheFirstMatchingEvent(t *testing.T) {
|
||||
if got.at.Sub(published) > time.Second {
|
||||
t.Errorf("answered %s after the event was published, want within a second", got.at.Sub(published))
|
||||
}
|
||||
var heard struct {
|
||||
Heard struct {
|
||||
Subject string `json:"subject"`
|
||||
Body map[string]any `json:"body"`
|
||||
At time.Time `json:"at"`
|
||||
} `json:"heard"`
|
||||
}
|
||||
var heard heardAnswer
|
||||
if err := json.Unmarshal([]byte(got.text), &heard); err != nil {
|
||||
t.Fatalf("not an answer: %s", got.text)
|
||||
}
|
||||
if heard.Heard.Subject != "mesh-knowledge.reviewed" || heard.Heard.Body["number"] != float64(367) || heard.Heard.At.IsZero() {
|
||||
t.Errorf("answered %+v, want record 367's verdict with its time", heard.Heard)
|
||||
if h := heard.Heard; h.Subject != "mesh-delivery-view.delivered" || h.From != "module" || h.Body["module"] != "netcheck" ||
|
||||
h.At.IsZero() || h.Published == nil || *h.Published != "2026-11-12T03:00:00.5Z" || !strings.Contains(heard.Read, "only wakes you") {
|
||||
t.Errorf("answered %s, want netcheck's delivery from a module with its times", got.text)
|
||||
}
|
||||
// And the subject it is granted on the asker's own `decided`, by the name a session asks for.
|
||||
// And a seat's `decided` of one asker, heard by `operator-channel.decided`: its whole name, from the seat, with no
|
||||
// time of its own, and the answer read only with claude_code_answer.
|
||||
before = s.conn.Subscriptions()
|
||||
go func() {
|
||||
text, isErr := callWait(s, map[string]any{"subjects": "operator-channel.decided.claude-code", "timeout": 20})
|
||||
text, isErr := callWait(s, map[string]any{"subjects": "operator-channel.decided", "timeout": 20})
|
||||
done <- answer{text, isErr, time.Now()}
|
||||
}()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
listening(t, s, before, 6)
|
||||
// A seat's event, said by the router on core NATS here: no stream of the test's bus carries the seat.
|
||||
if err := connect(t, "messenger", "").Say("mesh.seat.operator-channel.event.decided.claude-code", []byte(`{"ask":"a1"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case got = <-done:
|
||||
if got.isErr || !strings.Contains(got.text, `"subject": "operator-channel.decided.claude-code"`) {
|
||||
heard = heardAnswer{}
|
||||
_ = json.Unmarshal([]byte(got.text), &heard)
|
||||
if h := heard.Heard; got.isErr || h.Subject != "operator-channel.decided.claude-code" || h.From != "seat" ||
|
||||
h.Published != nil || !strings.Contains(heard.Read, "claude_code_answer") {
|
||||
t.Errorf("the asker's decided: %s", got.text)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
@@ -148,38 +180,64 @@ func TestMeshWaitsTimeoutIsBounded(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0367 §2 and §6: an event outside the granted families is refused, naming the families and how
|
||||
// one is added; no argument — a wildcard, another asker's `decided`, a subject spelled out — widens it.
|
||||
func TestMeshWaitRefusesAnEventOutsideTheGrantedFamilies(t *testing.T) {
|
||||
// novox/hq ADR 0670 §1, §2 (design 34 every-event §13, §16, §19): mesh_wait refuses loudly and never loops: a name with
|
||||
// a wildcard, of one token, more than 16 names, none, or a from of another word is refused at once naming the rule; a
|
||||
// wait whose every subscription the bus refuses answers after its timeout, in words naming no wait grant.
|
||||
func TestMeshWaitRefusesLoudlyAndNeverLoops(t *testing.T) {
|
||||
s, _ := waitingRuntime(t)
|
||||
for _, subjects := range []any{
|
||||
[]any{"mesh-knowledge.proposed"},
|
||||
[]any{"mesh-knowledge.reviewed", "gitea.forge.push"},
|
||||
[]any{"mesh-knowledge.*"},
|
||||
[]any{"operator-channel.decided.shop"},
|
||||
[]any{"operator-channel.decided.*"},
|
||||
[]any{"mesh.mod.mesh-knowledge.event.reviewed"},
|
||||
[]any{">"},
|
||||
[]any{},
|
||||
var seventeen []any
|
||||
for i := 0; i < 17; i++ {
|
||||
seventeen = append(seventeen, fmt.Sprintf("mesh-knowledge.reviewed%d", i))
|
||||
}
|
||||
for _, c := range []struct {
|
||||
args map[string]any
|
||||
says string
|
||||
}{
|
||||
{map[string]any{"subjects": []any{"mesh-knowledge.*"}}, "[A-Za-z0-9_-]+"},
|
||||
{map[string]any{"subjects": []any{"operator-channel.decided.>"}}, "[A-Za-z0-9_-]+"},
|
||||
{map[string]any{"subjects": []any{"mesh-knowledge"}}, "fewer than two tokens"},
|
||||
{map[string]any{"subjects": seventeen}, "at most 16"},
|
||||
{map[string]any{"subjects": []any{}}, "at least one event"},
|
||||
{map[string]any{"subjects": []any{"mesh-knowledge.reviewed"}, "from": "bench"}, "seat, module or stream"},
|
||||
} {
|
||||
text, isErr := callWait(s, map[string]any{"subjects": subjects, "timeout": 1})
|
||||
if !isErr {
|
||||
t.Errorf("%v was waited on: %s", subjects, text)
|
||||
continue
|
||||
c.args["timeout"] = 1
|
||||
start := time.Now()
|
||||
text, isErr := callWait(s, c.args)
|
||||
if !isErr || !strings.Contains(text, c.says) || strings.Contains(text, "manifest") {
|
||||
t.Errorf("%v: %s, want refused saying %q", c.args, text, c.says)
|
||||
}
|
||||
if len(subjects.([]any)) > 0 && (!strings.Contains(text, "mesh-knowledge.reviewed") || !strings.Contains(text, "manifest")) {
|
||||
t.Errorf("%v refused without naming the families and how one is added: %s", subjects, text)
|
||||
if took := time.Since(start); took > 500*time.Millisecond {
|
||||
t.Errorf("%v was refused after %s, want at once", c.args, took)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A tool runner issued no families waits on nothing, and says so.
|
||||
func TestMeshWaitWithNoFamiliesIssuedWaitsOnNothing(t *testing.T) {
|
||||
mt.New(t)
|
||||
s := NewSurface(connect(t, "node-tools", "desk"), nil, "desk.node-tools")
|
||||
text, isErr := callWait(s, map[string]any{"subjects": []any{"mesh-knowledge.reviewed"}, "timeout": 1})
|
||||
if !isErr || !strings.Contains(text, "no event families") {
|
||||
t.Errorf("a runtime issued no families: %s", text)
|
||||
// The bus refusing every subscription of the call: answered after the timeout, in §16's words.
|
||||
b, err := server.NewServer(&server.Options{Host: "127.0.0.1", Port: server.RANDOM_PORT, NoLog: true, NoSigs: true,
|
||||
Users: []*server.User{{Username: "desk.node-tools", Password: "pw", Permissions: &server.Permissions{
|
||||
Subscribe: &server.SubjectPermission{Allow: []string{"_INBOX.desk.node-tools.>", "mesh.assignment.desk.*"}},
|
||||
}}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go b.Start()
|
||||
if !b.ReadyForConnections(10 * time.Second) {
|
||||
t.Fatal("the bus did not come up")
|
||||
}
|
||||
t.Cleanup(b.Shutdown)
|
||||
conn, err := bus.Connect(bus.Credential{URL: b.ClientURL(), Module: "node-tools", Node: "desk", User: "desk.node-tools", Password: "pw"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Logf = func(string, ...any) {}
|
||||
t.Cleanup(conn.Close)
|
||||
start := time.Now()
|
||||
text, isErr := callWait(NewSurface(conn, nil, "desk.node-tools"), map[string]any{"subjects": []any{"mesh-delivery-view.delivered"}, "timeout": 1})
|
||||
if took := time.Since(start); took < time.Second || took > 3*time.Second {
|
||||
t.Errorf("a wait the bus refused whole answered after %s, want its timeout of 1s", took)
|
||||
}
|
||||
if !isErr || !strings.Contains(text, "the bus refused this account a subscription on mesh.seat.mesh-delivery-view.event.delivered") ||
|
||||
!strings.Contains(text, "novox/hq ADR 0670 §1") || strings.Contains(text, "wait grant") || strings.Contains(text, "0367 §2") {
|
||||
t.Errorf("a wait the bus refused whole: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user