Files
mesh-tools/node-tools/internal/runtime/seat_traffic_test.go
T
jochen 127047edd6
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/delivery-group group feat/asks-answered-on-any-channel stopped: a member was stopped
Serve a module from a runtime on its own account instead of switching users, keep bus words from bundles, and never give up a channel's work (hq ADR 0259 revision)
2026-10-09 10:12:39 +02:00

150 lines
5.8 KiB
Go

package runtime
import (
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
"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"
)
// A bundle's seat traffic through moduleBus.Seat (novox/hq ADR 0259 §3), against a real bus: publish under
// its own name with the id prefixed and a duplicate said; the record read without naming the bucket; a
// worker and a proof subject bound once however often a restarted child asks; and nothing beyond what the
// membership lists.
func TestABundlesSeatTrafficGoesThroughItsModulesBus(t *testing.T) {
mesh := mt.New(t)
nc, err := nats.Connect(mt.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, _ := nc.JetStream()
if _, err := js.AddStream(&nats.StreamConfig{Name: "SEAT_OPERATOR_CHANNEL", Retention: nats.WorkQueuePolicy,
Subjects: []string{"mesh.seat.operator-channel.accept.>"}}); err != nil {
t.Fatal(err)
}
if _, err := js.AddConsumer("SEAT_OPERATOR_CHANNEL", &nats.ConsumerConfig{Durable: "SEAT_OPERATOR_CHANNEL_worker",
AckPolicy: nats.AckExplicitPolicy, FilterSubject: "mesh.seat.operator-channel.accept.>"}); err != nil {
t.Fatal(err)
}
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: "messenger_asks"})
if err != nil {
t.Fatal(err)
}
_, _ = kv.Put("mesh-delivery.d-1", []byte(`{"state":"open"}`))
asker := mt.MembershipOf("mesh-delivery", "anchor", false, nil)
asker.SeatTraffic = &bus.SeatTraffic{Publish: []string{"mesh.seat.operator-channel.accept.ask.mesh-delivery"},
Records: []string{"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-delivery.>"}}
router := mt.MembershipOf("messenger", "anchor", false, nil)
router.SeatTraffic = &bus.SeatTraffic{Answers: []string{"mesh.seat.intake.proof.code.*"},
Workers: []bus.SeatWorker{{Stream: "SEAT_OPERATOR_CHANNEL", Consumer: "SEAT_OPERATOR_CHANNEL_worker",
Filter: "mesh.seat.operator-channel.accept.>"}}}
mesh.Issue(t, asker)
mesh.Issue(t, router)
conn := connect(t, "node-tools", "anchor")
conn.Follow("mesh-delivery")
conn.Follow("messenger")
mt.Until(t, func() error {
if conn.Membership("mesh-delivery") == nil || conn.Membership("messenger") == nil {
return errors.New("not issued yet")
}
return nil
})
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}
asking := all.forModule("mesh-delivery").(*moduleBus)
routing := all.forModule("messenger").(*moduleBus)
publish := json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-delivery","body":{"id":"d-1"},"id":"d-1"}`)
first, err := asking.Seat("publish", publish, nil)
if err != nil {
t.Fatal(err)
}
again, _ := asking.Seat("publish", publish, nil)
if !strings.Contains(string(first), `"duplicate":false`) || !strings.Contains(string(again), `"duplicate":true`) {
t.Errorf("the same id twice: %s, then %s", first, again)
}
if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-controller","body":{}}`), nil); err == nil {
t.Error("a module submitted under another's name")
}
got, err := asking.Seat("record", json.RawMessage(`{"key":"d-1"}`), nil)
if err != nil || !strings.Contains(string(got), `"state":"open"`) {
t.Errorf("the record read without its bucket: %s %v", got, err)
}
var mu sync.Mutex
var taken []string
handOn := func(method string, params any) (json.RawMessage, error) {
mu.Lock()
defer mu.Unlock()
raw, _ := json.Marshal(params)
taken = append(taken, method+" "+string(raw))
return json.RawMessage(`{}`), nil
}
for i := 0; i < 2; i++ { // a child that started again asks again
if _, err := routing.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err != nil {
t.Fatal(err)
}
}
deadline := time.Now().Add(10 * time.Second)
for {
mu.Lock()
n := len(taken)
mu.Unlock()
if n >= 1 || time.Now().After(deadline) {
break
}
time.Sleep(50 * time.Millisecond)
}
time.Sleep(300 * time.Millisecond)
mu.Lock()
if len(taken) != 1 || !strings.Contains(taken[0], "mesh/work") || !strings.Contains(taken[0], `"x-event-id":"mesh-delivery.d-1"`) {
t.Errorf("taken %v", taken)
}
mu.Unlock()
if _, err := asking.Seat("take", json.RawMessage(`{"worker":"SEAT_OPERATOR_CHANNEL_worker"}`), handOn); err == nil {
t.Error("an asker took the router's work")
}
if _, err := asking.Seat("answer", json.RawMessage(`{"subject":"mesh.seat.intake.proof.code.*"}`), handOn); err == nil {
t.Error("an asker answered proofs")
}
if _, err := asking.Seat("anything", json.RawMessage(`{}`), nil); err == nil {
t.Error("an unknown verb was answered")
}
}
// A lost compare-and-set is answered with its code, so the SDK knows it without reading words.
func TestALostCompareAndSetCarriesItsCode(t *testing.T) {
mesh := mt.New(t)
mesh.Bucket(t, "messenger_asks")
m := mt.MembershipOf("messenger", "anchor", false, nil)
m.State = []bus.StateIssued{{Name: "asks", Bucket: "messenger_asks", Writes: true}}
mesh.Issue(t, m)
conn := connect(t, "node-tools", "anchor")
conn.Follow("messenger")
mt.Until(t, func() error {
if conn.Membership("messenger") == nil {
return errors.New("not issued yet")
}
return nil
})
b := (&consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}).forModule("messenger")
if _, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`)); err != nil {
t.Fatal(err)
}
_, err := b.State("create", json.RawMessage(`{"state":"asks","key":"a","value":{}}`))
var coded interface{ ErrorCode() int }
if !errors.As(err, &coded) || coded.ErrorCode() != -32010 || !errors.Is(err, bus.ErrStateChanged) {
t.Errorf("a second create: %v", err)
}
if _, err := b.State("update", json.RawMessage(`{"state":"asks","key":"a","value":{},"revision":0}`)); err == nil {
t.Error("an update of revision zero was taken")
}
}