HearFirst reads the event's x-time as Published, notes a subscription the bus refuses while the others go on waiting, and answers a wait refused whole only after its timeout, in words naming the grants part's grant of every event subject; mesh_wait lists the refused subjects in its answer. Builds: design 34 every-event §12, §16, §18, §19 (bus/wait_test.go) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
118 lines
4.4 KiB
Go
118 lines
4.4 KiB
Go
package bus_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// novox/hq ADR 0670 §1 (design 34 every-event §12, §16): a subscription the bus refuses is noted and the others go on
|
|
// waiting, a heard event carries its own time; a wait whose every subscription is refused answers only after its
|
|
// timeout, in words naming the grants part's grant of every event subject, never a wait grant (E-2).
|
|
func TestTheBusRefusesAWaitOutsideTheGrant(t *testing.T) {
|
|
granted := "mesh.seat.operator-channel.event.decided.claude-code"
|
|
other := "mesh.seat.operator-channel.event.decided.shop"
|
|
s, 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{
|
|
Publish: &server.SubjectPermission{Allow: []string{"$JS.API.DIRECT.GET.>", "mesh.>"}},
|
|
Subscribe: &server.SubjectPermission{Allow: []string{granted, "_INBOX.desk.node-tools.>", "mesh.assignment.desk.*"}},
|
|
}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.Start()
|
|
if !s.ReadyForConnections(10 * time.Second) {
|
|
t.Fatal("the bus did not come up")
|
|
}
|
|
t.Cleanup(s.Shutdown)
|
|
c := dial(t, s.ClientURL(), "node-tools", "desk", "desk.node-tools")
|
|
|
|
// Every subscription refused: answered after the timeout, never at once, in §16's words.
|
|
start := time.Now()
|
|
_, ok, refused, err := c.HearFirst(context.Background(), []string{other}, 300*time.Millisecond, nil)
|
|
if took := time.Since(start); took < 300*time.Millisecond || took > 3*time.Second {
|
|
t.Errorf("a wait refused whole answered after %s, want its timeout of 300ms", took)
|
|
}
|
|
if ok || err == nil || !strings.Contains(err.Error(), "the bus refused this account a subscription on "+other) ||
|
|
!strings.Contains(err.Error(), "every event subject (novox/hq ADR 0670 §1)") ||
|
|
strings.Contains(err.Error(), "wait grant") || strings.Contains(err.Error(), "0367") ||
|
|
len(refused) != 1 || refused[0] != other {
|
|
t.Errorf("a wait on another asker's decided: heard %v, refused %v, %v", ok, refused, err)
|
|
}
|
|
|
|
// One refused beside one granted: the granted one hears, with the event's own time, and the refused one is listed.
|
|
pub, err := nats.Connect(s.ClientURL(), nats.UserInfo("desk.node-tools", "pw"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(pub.Close)
|
|
for _, published := range []string{"2026-11-12T03:00:00.5Z", ""} {
|
|
type result struct {
|
|
h bus.Heard
|
|
refused []string
|
|
err error
|
|
}
|
|
done := make(chan result, 1)
|
|
go func() {
|
|
h, ok, refused, err := c.HearFirst(context.Background(), []string{other, granted}, 5*time.Second, nil)
|
|
if !ok && err == nil {
|
|
err = errors.New("timed out")
|
|
}
|
|
done <- result{h, refused, err}
|
|
}()
|
|
// The event is said again until the wait, subscribed in its own time, hears it.
|
|
say := time.NewTicker(20 * time.Millisecond)
|
|
var r result
|
|
heard:
|
|
for {
|
|
select {
|
|
case r = <-done:
|
|
break heard
|
|
case <-say.C:
|
|
msg := nats.NewMsg(granted)
|
|
msg.Data = []byte(`{"ask":"a1"}`)
|
|
if published != "" {
|
|
msg.Header.Set("x-time", published)
|
|
}
|
|
if err := pub.PublishMsg(msg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
say.Stop()
|
|
if r.err != nil || r.h.Subject != granted || string(r.h.Body) != `{"ask":"a1"}` || r.h.Published != published {
|
|
t.Errorf("heard %+v, %v; want published %q", r.h, r.err, published)
|
|
}
|
|
if len(r.refused) > 1 || len(r.refused) == 1 && r.refused[0] != other {
|
|
t.Errorf("refused %v, want at most %s", r.refused, other)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A wait whose caller is gone ends at once, its subscription closed (the review of issue 745).
|
|
func TestAWaitEndsWhenItsCallerIsGone(t *testing.T) {
|
|
c := dial(t, mt.URL(t), "node-tools", "desk", "")
|
|
before := c.Subscriptions()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
time.AfterFunc(200*time.Millisecond, cancel)
|
|
start := time.Now()
|
|
_, ok, _, err := c.HearFirst(ctx, []string{"mesh.mod.mesh-knowledge.event.reviewed"}, time.Minute, nil)
|
|
if ok || !errors.Is(err, context.Canceled) {
|
|
t.Errorf("a cancelled wait answered %v, %v", ok, err)
|
|
}
|
|
if took := time.Since(start); took > 2*time.Second {
|
|
t.Errorf("a cancelled wait ended after %s", took)
|
|
}
|
|
if after := c.Subscriptions(); after != before {
|
|
t.Errorf("%d subscriptions before, %d after", before, after)
|
|
}
|
|
}
|