package bus_test import ( "context" "encoding/json" "errors" "strings" "testing" "time" "github.com/nats-io/nats-server/v2/server" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) // novox/hq ADR 0367 §2, §5: the bus refuses the tool runner's account any subject its wait grant does not // name — another asker's `decided` among them — and a wait on one says the bus refused it, at once. func TestTheBusRefusesAWaitOutsideTheGrant(t *testing.T) { granted := "mesh.seat.operator-channel.event.decided.claude-code" 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") start := time.Now() _, ok, err := c.HearFirst(context.Background(), []string{"mesh.seat.operator-channel.event.decided.shop"}, 5*time.Second, nil) if ok || err == nil || !strings.Contains(err.Error(), "refused") { t.Errorf("a wait on another asker's decided: heard %v, %v", ok, err) } if time.Since(start) > 3*time.Second { t.Errorf("the refusal took %s to be said", time.Since(start)) } done := make(chan bus.Heard, 1) go func() { h, ok, err := c.HearFirst(context.Background(), []string{granted}, 5*time.Second, func(body json.RawMessage) bool { return true }) if ok && err == nil { done <- h } }() time.Sleep(300 * time.Millisecond) if err := c.Say(granted, []byte(`{"ask":"a1"}`)); err != nil { t.Fatal(err) } select { case h := <-done: if h.Subject != granted || string(h.Body) != `{"ask":"a1"}` { t.Errorf("heard %+v", h) } case <-time.After(5 * time.Second): t.Fatal("a wait on the granted subject heard nothing") } } // 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) } }