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) } }