mesh/merge-gate head 21d038d3397c merged into trunk 5d70eb3e35a9 — pass, with a note: builds mesh-tools, node-tools (+39 dependent(s)) → ace, g14, novox, s…
mesh/repo-check head 21d038d3397c merged into trunk 5d70eb3e35a9 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
Closing the console shut its server down with a context that never ends, so a stop or restart of the tool runner waited on any mesh_wait in flight for up to ten minutes. The server's base context is now cancelled first, ending every wait at once, with a five-second shutdown as a backstop (second review of issue 745). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.0 KiB
Go
84 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|