Files
mesh-delivery/modules/mesh-grants/cmd/mesh-grants/integration_test.go
T
jschoubben ca9aad9757 1966 subtask 2: the grants part composes a binding from an answer of mesh-broker's list without binding
An answer no entry of which carries binding gets, for a key-value or object-store
entry whose settings hold the bus object its bus-name names, the binding composed
from the answer's own fields; a stream, a ledger and an entry not held stay
unbound and are logged once per answer; grants_first_views shows composed and
unbound (1966/AC203-3..n). An answer where any entry carries binding is applied
by §4 alone.

Builds: design 25 bindings-first-view §9, §4, §6, §7, §8

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 13:58:23 +01:00

591 lines
27 KiB
Go

//go:build integration
package main
import (
"context"
"encoding/json"
"errors"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/membership"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seal"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-grants/internal/grants"
)
// TestTheGrantsPartWhole starts the program the way the node does (its entry, run, with its environment and a broker
// file) against an embedded bus that holds no controller at all: no lease bucket, no controller placed (novox/hq ADR 0590,
// issue 1126). Given a module placed with an event it consumes, it publishes the module's membership in its own state and
// on no subject, makes the module's durable consumer on EVENTS, and writes the sealed list's key for the bus node to follow
// alone; and it creates no lease, nor any state of a lease write or a take-over.
func TestTheGrantsPartWhole(t *testing.T) {
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "grants")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
srv, err := server.NewServer(&server.Options{Host: "127.0.0.1", Port: -1, JetStream: true, StoreDir: filepath.Join(dir, "js"), NoLog: true, NoSigs: true})
if err != nil {
t.Fatal(err)
}
go srv.Start()
if !srv.ReadyForConnections(10 * time.Second) {
t.Fatal("the bus did not come up")
}
t.Cleanup(srv.Shutdown)
nc, err := nats.Connect(srv.ClientURL())
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, _ := jetstream.New(nc)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
kvs := map[string]jetstream.KeyValue{}
for _, state := range append(append([]string(nil), grants.Reads...), grants.ViewReads...) {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucketOf(state), History: 5})
if err != nil {
t.Fatal(err)
}
kvs[state] = kv
}
for _, state := range grants.States {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: grants.Module + "_" + state, History: 5})
if err != nil {
t.Fatal(err)
}
kvs[grants.Module+"."+state] = kv
}
for _, c := range []jetstream.StreamConfig{
{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}},
{Name: "CONDITIONS", Subjects: []string{"mesh.seat.operator-channel.accept.condition.>"}},
} {
if _, err := js.CreateStream(ctx, c); err != nil {
t.Fatal(err)
}
}
put := func(state, key string, v any) {
t.Helper()
raw, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
if _, err := kvs[state].Put(ctx, key, raw); err != nil {
t.Fatal(err)
}
}
manifest := func(module, raw string) {
digest := meshcatalogclient.ManifestDigest([]byte(raw))
put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: module, Manifest: raw})
put(grants.ReadModules, module, meshcatalog.ModulesByModuleValue{V: keys.Version, Fp2: "src2:" + strings.Repeat("ab", 32), Manifest: digest,
Repository: "novox/x", Commit: "c"})
verdict, _ := meshcatalogclient.EncodeVerdict(meshcatalog.VerdictsByDigestValue{Module: module, State: meshcatalogclient.VerdictAccepted, Judge: "j", At: new(time.Now())})
put(grants.ReadVerdicts, digest, json.RawMessage(verdict))
}
manifest("nats", `{"module":"nats","version":"1","claims":[{"name":"mesh-broker"}],"own-secrets":{"broker":"/var/lib/nats/broker"},`+
`"emits":["created","done","dropped","recreated"],"streams":[{"name":"broker-events","events":["created","dropped","recreated"],"max-age-days":30,"max-mib":256}]}`)
manifest("mod-c", `{"module":"mod-c","version":"1","own-secrets":{"broker":"/var/lib/c/broker"},"consumes":["nats.done"]}`)
// The part itself, placed as on the mesh, so its own consumer on EVENTS hears the seat mesh-broker's events (DLV-G1).
own, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
manifest(grants.Module, string(own))
values, _ := meshmoduleassignerclient.AssignmentsValues(meshmoduleassigner.Assignments{Node: "a", Modules: []meshmoduleassigner.PlacedModule{{Module: "nats"}, {Module: "mod-c"},
{Module: grants.Module}}})
for _, k := range slices.Sorted(maps.Keys(values)) { // a key per module, then the node key
if _, err := kvs[grants.ReadAssignments].Put(ctx, k, values[k]); err != nil {
t.Fatal(err)
}
}
busKey, _ := seal.GenerateKeyPair()
put(grants.ReadHave, "a.node", map[string]any{"v": 1})
put(grants.ReadHave, "a.nats", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK,
UserList: &nodeengine.UserListWritten{Source: grantsstate.GrantsUserListSource, Revision: 1, Digest: "sha256:aa", Users: 1,
Key: busKey.PublicKey, Follows: grantsstate.FollowGrants}})
// The bus module's read tools, as nats-tools answers them: the streams with their consumers as the bus holds them now.
answer := func(m *nats.Msg, result any) {
raw, _ := json.Marshal(map[string]any{"result": result, "node": "a"})
_ = m.Respond(raw)
}
sub, err := nc.Subscribe("mesh.mod.nats.>", func(m *nats.Msg) {
switch {
case strings.Contains(m.Subject, ".nats_users"):
answer(m, map[string]any{"users": []any{}})
case strings.Contains(m.Subject, ".nats_streams"):
var args struct{ Stream string }
_ = json.Unmarshal(m.Data, &args)
detail := []any{}
if st, err := js.Stream(ctx, args.Stream); err == nil {
consumers := []any{}
names := st.ConsumerNames(ctx)
for name := range names.Name() {
c, _ := st.Consumer(ctx, name)
cfg := c.CachedInfo().Config
consumers = append(consumers, map[string]any{"name": name, "filters": cfg.FilterSubjects, "max_deliver": cfg.MaxDeliver})
}
detail = append(detail, map[string]any{"name": args.Stream, "consumer_detail": consumers})
}
answer(m, map[string]any{"detail": detail})
default:
answer(m, map[string]any{})
}
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sub.Unsubscribe() })
// DLV-F4r, DLV-F5r (ADR 0615 §4, §6, ADR 0621 §3): the owners answer their first-view tools at once from what they
// hold, with its revision, as the indexer's and the assigner's runtimes of their own do; the part never reads their
// buckets. Started before its owners answer (answering false: each ask refused, as an owner not yet connected), the
// part waits, says so, and composes nothing it would read from them.
var firstViewAsks, refusedAsks atomic.Int32
var answering atomic.Bool
for _, owner := range []string{grants.IndexerModule, grants.AssignerModule} {
owner := owner
osub, err := nc.Subscribe("mesh.mod."+owner+".>", func(m *nats.Msg) {
for _, fv := range grants.FirstViews {
if fv.Owner != owner || !strings.Contains(m.Subject, "."+fv.State) {
continue
}
if !answering.Load() {
refusedAsks.Add(1)
raw, _ := json.Marshal(map[string]any{"error": "not connected yet", "node": "a"})
_ = m.Respond(raw)
return
}
firstViewAsks.Add(1)
values, rev := map[string]json.RawMessage{}, uint64(0)
if keys, err := kvs[fv.Read].ListKeys(ctx); err == nil {
for k := range keys.Keys() {
if e, err := kvs[fv.Read].Get(ctx, k); err == nil {
values[k] = e.Value()
rev = max(rev, e.Revision())
}
}
}
answer(m, views.Answer{V: views.Version, Owner: owner, State: fv.State, Revision: rev,
Created: time.Date(2026, 11, 4, 0, 0, 0, 0, time.UTC), Values: values, At: time.Now()})
return
}
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = osub.Unsubscribe() })
}
// D-30 (novox/hq ADR 0636 §1): the seat mesh-catalog's holder answers the seat list with no trunk seat named, so the reader
// has heard the seat and holds the bootstrapped definitions, and every module is composed. (The unit case holds a module
// while the list is unread.)
var seatAnswering atomic.Bool
seatAnswering.Store(false)
for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} {
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
if !seatAnswering.Load() {
_ = m.Respond([]byte(`{"error":"not connected yet","node":"a"}`))
return
}
answer(m, map[string]any{"revision": 1, "seats": []any{}})
}); err != nil {
t.Fatal(err)
}
}
// Design 25 bindings-first-view §8: the bus module answers the seat mesh-broker's list with an entry whose created was never
// said, once it is answering; until then the ask is refused as of a holder not yet connected.
var listing atomic.Bool
// A gate on the bus module's answer: while set, the answer waits (after saying it was asked) until the test releases it.
type listGate struct{ asked, release chan struct{} }
var gate atomic.Pointer[listGate]
var answeredGated, listAsked, listAnswered atomic.Int32
// While set, the bus module answers as a build from before the field `binding` (design 25 bindings-first-view §9).
var oldBuild atomic.Bool
for _, subject := range []string{"mesh.seat.mesh-broker.call.list.>", "mesh.seat.mesh-broker.tool.list"} {
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
if !listing.Load() {
_ = m.Respond([]byte(`{"error":"not connected yet","node":"a"}`))
return
}
listAsked.Add(1)
defer listAnswered.Add(1)
var more []any
if oldBuild.Load() {
answer(m, map[string]any{"entries": []any{
map[string]any{"module": "mod-c", "local-name": "late", "storage-type": "key-value", "template": "state", "bus-name": "mod-c_late",
"settings": map[string]any{"KV_mod-c_late": map[string]any{}}},
map[string]any{"module": "mod-c", "local-name": "feed", "storage-type": "stream", "bus-name": "mod-c_feed",
"settings": map[string]any{"mod-c_feed": map[string]any{}}},
}})
return
}
if g := gate.Load(); g != nil {
g.asked <- struct{}{}
<-g.release
defer answeredGated.Add(1)
// The held answer lists one entry more (waiting, no binding), so its taking shows in the kept status.
more = append(more, map[string]any{"module": "mod-c", "local-name": "also-waiting", "storage-type": "key-value",
"template": "state", "bus-name": "mod-c_also-waiting", "state": "waiting"})
}
answer(m, map[string]any{"entries": append([]any{
map[string]any{"module": "mod-c", "local-name": "late", "storage-type": "key-value", "template": "state", "bus-name": "mod-c_late",
"settings": map[string]any{"mod-c_late": map[string]any{}},
"binding": map[string]any{"local": "late", "storage": "key-value", "template": "state", "bucket": "mod-c_late"}},
map[string]any{"module": "mod-c", "local-name": "waiting", "storage-type": "key-value", "template": "state", "bus-name": "mod-c_waiting",
"state": "waiting"},
}, more...)})
}); err != nil {
t.Fatal(err)
}
}
// The conditions the part said, by key, as last said.
conditionsSaid := func() map[string]string {
out := map[string]string{}
st, err := js.Stream(ctx, "CONDITIONS")
if err != nil {
return out
}
info, _ := st.Info(ctx)
for seq := info.State.FirstSeq; seq != 0 && seq <= info.State.LastSeq; seq++ {
raw, err := st.GetMsg(ctx, seq)
if err != nil {
continue
}
if c, err := occ.DecodeCondition(raw.Data); err == nil {
out[c.Key] = string(c.State)
}
}
return out
}
bindingsBoundAndWaits := func() {
bindingsTiming.bound, bindingsTiming.wait, bindingsTiming.max = 10*time.Second, 50*time.Millisecond, 100*time.Millisecond
}
bindingsBoundAndWaits()
t.Cleanup(func() { bindingsTiming.bound, bindingsTiming.wait, bindingsTiming.max = 0, 0, 0 })
// A membership is never said on its subject any more (issue 1126).
var said atomic.Int32
if _, err := nc.Subscribe("mesh.assignment.>", func(*nats.Msg) { said.Add(1) }); err != nil {
t.Fatal(err)
}
broker := filepath.Join(dir, "broker")
cred, _ := json.Marshal(process.Account{URL: srv.ClientURL(), Node: "a", Module: grants.Module, User: "a." + grants.Module, Password: "pw"})
if err := os.WriteFile(broker, cred, 0o600); err != nil {
t.Fatal(err)
}
env := map[string]string{"MESH_BROKER_FILE": broker}
runCtx, stop := context.WithCancel(ctx)
done := make(chan error, 1)
logged := &lockedLog{}
go func() { done <- run(runCtx, func(k string) string { return env[k] }, logged) }()
t.Cleanup(func() { stop(); <-done })
until := func(what string, ok func() bool) {
t.Helper()
for deadline := time.Now().Add(30 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if ok() {
return
}
}
t.Fatalf("never saw: %s\n%s", what, logged.tail("binding"))
}
until("every first view asked and refused once", func() bool { return refusedAsks.Load() >= int32(len(grants.FirstViews)) })
until("the part saying it waits on the indexer and the assigner", func() bool {
return logged.has("mesh-delivery-indexer.manifests") && logged.has("mesh-delivery-assigner.assignments") && logged.has("is not answered")
})
if _, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c"); err == nil {
t.Error("a membership composed before the owners answered its first views")
}
// ADR 0636 §1, the unread phase: the seat was silent when the part started and speaks now, and says so on its event
// before the part's own consumer exists (the part makes it from its first composition, which waits on its owners'
// first views). The event is lost to that consumer, which starts at the stream's end, so the part reads the list again
// when its consumer is heard; mod-c, which the unread list held, is composed after that and not before.
seatAnswering.Store(true)
if _, err := js.Publish(ctx, "mesh.seat.mesh-catalog.event.seat-definition-changed", []byte(`{"seat":"mesh-broker","source":"trunk"}`)); err != nil {
t.Fatal(err)
}
answering.Store(true)
until("a.mod-c's membership in the part's own state memberships", func() bool {
e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c")
if err != nil {
return false
}
m, err := membership.DecodeGrantsMembership(e.Value())
return err == nil && m.Module == "mod-c"
})
until("mod-c's durable consumer on EVENTS, made by the part", func() bool {
_, err := js.Consumer(ctx, "EVENTS", "a_mod-c")
return err == nil
})
until("the sealed list's key, followed by the bus node alone", func() bool {
e, err := kvs[grants.Module+"."+grants.StateUserList].Get(ctx, grants.KeySealed)
if err != nil {
return false
}
g, err := grantsstate.DecodeGrantsUserList(e.Value())
return err == nil && g.Follow == grantsstate.FollowGrants
})
if n := said.Load(); n != 0 {
t.Errorf("%d memberships were said on their subject", n)
}
if firstViewAsks.Load() < int32(len(grants.FirstViews)) {
t.Errorf("the part composed with %d first-view asks, want one of each of the %d", firstViewAsks.Load(), len(grants.FirstViews))
}
// DLV-G1, 1966/AC34 (novox/hq ADR 0595 §1, §7): the bus module's created, said on the seat mesh-broker's subject on
// EVENTS like every seat's event (the lead's ruling, registry milestone 13:0x), reaches the part on its durable consumer
// there, and the binding it carries is in the module's membership; dropped takes it out.
until("the part's durable consumer on EVENTS hearing mesh-broker's created", func() bool {
cons, err := js.Consumer(ctx, "EVENTS", "a_"+grants.Module)
if err != nil {
return false
}
info, err := cons.Info(ctx)
return err == nil && slices.Contains(info.Config.FilterSubjects, "mesh.seat.mesh-broker.event.created")
})
bindingOf := func() []membership.BusBinding {
e, err := kvs[grants.Module+"."+grants.StateMemberships].Get(ctx, "a.mod-c")
if err != nil {
return nil
}
m, _ := membership.DecodeGrantsMembership(e.Value())
return m.Bindings
}
sayBroker := func(event string, bind membership.BusBinding) {
body, _ := json.Marshal(grants.BrokerEvent{V: 1, Module: "mod-c", Local: bind.Local, Storage: "key-value", Template: "state", Binding: bind})
if _, err := js.Publish(ctx, "mesh.seat.mesh-broker.event."+event, body); err != nil {
t.Fatal(err)
}
}
// With list unanswered the part has composed every account (mod-c's membership is above), says past its bound that the
// bindings' first view is unanswered, naming the seat and the verb, and applies the created heard meanwhile.
if said := conditionsSaid()[grants.FirstViewUnanswered+".mesh-broker.bindings"]; said != "" {
t.Fatalf("first-view-unanswered %q before the created is said: the bound (10s) passed first", said)
}
sayBroker("created", membership.BusBinding{Local: "kept", Storage: "key-value", Template: "state", Bucket: "mod-c_kept"})
// The created is held until the bound (§3): when it is applied, first-view-unanswered is already raised.
until("mod-c's binding in its membership after created, list unanswered", func() bool {
b := bindingOf()
return len(b) == 1 && b[0].Bucket == "mod-c_kept"
})
if said := conditionsSaid()[grants.FirstViewUnanswered+".mesh-broker.bindings"]; said != "raised" {
t.Fatalf("the created held while list is unanswered was applied before the bound: first-view-unanswered %q, want raised", said)
}
// The bus module answers: the entry whose created was never said gets its binding; the one the answer does not list (kept)
// stays and raises binding-unlisted; an entry listed without a binding changes nothing; the answer clears the first
// view's condition.
listing.Store(true)
until("the binding of the entry created never said, kept beside the one the answer does not list", func() bool {
b := bindingOf()
return len(b) == 2 && b[0].Bucket == "mod-c_kept" && b[1].Bucket == "mod-c_late"
})
until("binding-unlisted raised for the kept binding and first-view-unanswered cleared", func() bool {
said := conditionsSaid()
return said[grants.BindingUnlisted+".mod-c.kept"] == "raised" && said[grants.FirstViewUnanswered+".mesh-broker.bindings"] == "cleared" &&
said[grants.BindingUnlisted+".mod-c.late"] == "" && said[grants.BindingUnlisted+".mod-c.waiting"] == ""
})
// A person asks again by hand (state bindings): asked once more, the same answer changes nothing it holds.
askedBefore := listAsked.Load()
askedAgain, _ := json.Marshal(grants.Ask{By: "someone@desk", At: time.Now(), FirstView: grants.FirstViewBindings})
if _, err := kvs[grants.Module+"."+grants.StateUserList].Put(ctx, grants.KeyAsked, askedAgain); err != nil {
t.Fatal(err)
}
until("the bindings asked once more by hand", func() bool {
return logged.has("the first view of the bindings (" + grants.CallBrokerList + ") once more by hand")
})
sayBroker("dropped", membership.BusBinding{Local: "kept"})
until("mod-c's kept binding gone after dropped, its unlisted condition cleared", func() bool {
b := bindingOf()
return len(b) == 1 && b[0].Bucket == "mod-c_late" && conditionsSaid()[grants.BindingUnlisted+".mod-c.kept"] == "cleared"
})
until("the ask by hand answered", func() bool { return listAsked.Load() > askedBefore && listAnswered.Load() == listAsked.Load() })
// An answer that comes after a dropped heard since the ask does not write the dropped binding back (§4): the bus module
// holds the answer, which still lists the entry, while the dropped is said and heard.
g := &listGate{asked: make(chan struct{}, 1), release: make(chan struct{})}
gate.Store(g)
askedAgain, _ = json.Marshal(grants.Ask{By: "someone@desk", At: time.Now().Add(time.Second), FirstView: grants.FirstViewBindings})
if _, err := kvs[grants.Module+"."+grants.StateUserList].Put(ctx, grants.KeyAsked, askedAgain); err != nil {
t.Fatal(err)
}
select {
case <-g.asked:
case <-time.After(20 * time.Second):
t.Fatalf("the bus module was not asked again by hand\n%s", logged.tail("by hand")+logged.tail("bindings"))
}
sayBroker("dropped", membership.BusBinding{Local: "late"})
until("mod-c's late binding gone after dropped, the answer held", func() bool { return len(bindingOf()) == 0 })
close(g.release)
gate.Store(nil)
until("the held answer taken, as the kept status of the first views says", func() bool {
e, err := kvs[grants.Module+"."+grants.StateUserList].Get(ctx, grants.KeyFirstViews)
if err != nil {
return false
}
var st grants.FirstViewsState
return json.Unmarshal(e.Value(), &st) == nil && st.Bindings != nil && st.Bindings.Listed == 3 && st.Bindings.Error == ""
})
if b := bindingOf(); len(b) != 0 {
t.Fatalf("after an answer that lists a binding dropped since its ask: %+v", b)
}
// 1966/AC203-3..: a bus module's build from before the field answers no binding on any entry (design 25 bindings-first-view
// §9): the key-value entry the bus holds gets its binding composed from the answer's fields, the stream is left unbound.
oldBuild.Store(true)
askedAgain, _ = json.Marshal(grants.Ask{By: "someone@desk", At: time.Now().Add(2 * time.Second), FirstView: grants.FirstViewBindings})
if _, err := kvs[grants.Module+"."+grants.StateUserList].Put(ctx, grants.KeyAsked, askedAgain); err != nil {
t.Fatal(err)
}
until("the binding of the entry composed from an answer with no binding, the stream left unbound", func() bool {
e, err := kvs[grants.Module+"."+grants.StateUserList].Get(ctx, grants.KeyFirstViews)
if err != nil {
return false
}
var st grants.FirstViewsState
b := bindingOf()
return json.Unmarshal(e.Value(), &st) == nil && st.Bindings != nil && st.Bindings.Composed == 1 &&
slices.Equal(st.Bindings.Unbound, []string{"mod-c/feed (stream)"}) && len(b) == 1 && b[0].Bucket == "mod-c_late"
})
for _, bucket := range []string{"mesh-controller_lease", grants.Module + "_lease", grants.Module + "_takeover"} {
if _, err := js.KeyValue(ctx, bucket); !errors.Is(err, jetstream.ErrBucketNotFound) {
t.Errorf("the bucket %s: %v, want none", bucket, err)
}
}
// Issue 1966: the seat list's again tool answers an operator's session, which the mesh MCP server proves as its
// machine's tool runner (issue 627), and the healers; no other module.
t.Run("the seat list's again tool answers an operator's session", func(t *testing.T) {
for caller, refused := range map[string]bool{"person.jochen": false, "shanks.node-tools": false, "ace.mesh-healers": false,
"ace.messenger": true} {
if err := seatlist.Refusal(grants.ToolSeatListAgain, seatListAgainCallers, caller); (err != nil) != refused {
t.Errorf("%s: refused=%v, err %v", caller, refused, err)
}
}
})
// ADR 0640 §5: the real additions change the subjects and the filters and nothing else, and recreate nothing.
t.Run("adding subjects and filters changes only those", func(t *testing.T) {
stream, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ADDS", Subjects: []string{"a.>"}, MaxAge: time.Hour, MaxMsgs: 100,
Retention: jetstream.LimitsPolicy, Discard: jetstream.DiscardNew, MaxBytes: 1 << 20})
if err != nil {
t.Fatal(err)
}
cons, err := stream.CreateConsumer(ctx, jetstream.ConsumerConfig{Durable: "idx", AckPolicy: jetstream.AckExplicitPolicy, FilterSubject: "a.one", MaxDeliver: 7})
if err != nil {
t.Fatal(err)
}
for range 3 {
if _, err := js.Publish(ctx, "a.one", []byte("x")); err != nil {
t.Fatal(err)
}
}
got, err := cons.Fetch(2)
if err != nil {
t.Fatal(err)
}
for m := range got.Messages() {
_ = m.Ack()
}
before, _ := stream.Info(ctx)
cbefore, _ := cons.Info(ctx)
if cbefore.Delivered.Stream != 2 {
t.Fatalf("the consumer delivered %d, want 2", cbefore.Delivered.Stream)
}
c := creating{ctx: ctx, js: js}
if err := c.AddSubjects("ADDS", []string{"b.>"}); err != nil {
t.Fatal(err)
}
if err := c.AddFilters("ADDS", "idx", []string{"a.two"}); err != nil {
t.Fatal(err)
}
after, _ := stream.Info(ctx)
cafter, _ := cons.Info(ctx)
b, a := before.Config, after.Config
if !slices.Equal(a.Subjects, []string{"a.>", "b.>"}) || a.MaxAge != b.MaxAge || a.MaxMsgs != b.MaxMsgs || a.Retention != b.Retention ||
a.Discard != b.Discard || !after.Created.Equal(before.Created) || after.State.Msgs != before.State.Msgs || after.State.LastSeq != before.State.LastSeq {
t.Errorf("the stream after: %+v created %v, before %+v created %v", a, after.Created, b, before.Created)
}
cb, ca := cbefore.Config, cafter.Config
if !slices.Equal(ca.FilterSubjects, []string{"a.one", "a.two"}) || ca.FilterSubject != "" || ca.MaxDeliver != cb.MaxDeliver || ca.AckPolicy != cb.AckPolicy ||
cafter.Delivered.Stream != cbefore.Delivered.Stream || cafter.AckFloor.Stream != cbefore.AckFloor.Stream || !cafter.Created.Equal(cbefore.Created) {
t.Errorf("the consumer after: %+v delivered %d created %v, before %+v delivered %d created %v", ca, cafter.Delivered.Stream, cafter.Created, cb, cbefore.Delivered.Stream, cbefore.Created)
}
// A size cap is only ever raised, and nothing else changes with it (the operator, 2026-11-09).
for _, row := range []struct {
to, from, want int64
raised bool
}{{4 << 20, 1 << 20, 4 << 20, true}, {2 << 20, 4 << 20, 4 << 20, false}} {
from, raised, err := c.RaiseMaxBytes("ADDS", row.to)
if err != nil {
t.Fatal(err)
}
now, _ := stream.Info(ctx)
if from != row.from || raised != row.raised || now.Config.MaxBytes != row.want || now.Config.MaxAge != b.MaxAge ||
!slices.Equal(now.Config.Subjects, []string{"a.>", "b.>"}) || !now.Created.Equal(before.Created) || now.State.Msgs != before.State.Msgs {
t.Errorf("raise to %d: from %d raised %v, config %+v", row.to, from, raised, now.Config)
}
}
// Nothing is removed: a second addition of the same and of a subset keeps what is there.
if err := c.AddSubjects("ADDS", []string{"a.>"}); err != nil {
t.Fatal(err)
}
if again, _ := stream.Info(ctx); !slices.Equal(again.Config.Subjects, []string{"a.>", "b.>"}) {
t.Errorf("subjects after adding a held one: %v", again.Config.Subjects)
}
})
// And what the part composes holds on a server that loads it (one integration test per module, the test budget).
t.Run("the composed grants hold on a server that loads them", theBusNodeFollowsTheSealedListUnderTheComposedGrants)
}
// lockedLog is the part's log, read by the test while the part writes it.
type lockedLog struct {
mu sync.Mutex
buf strings.Builder
}
func (l *lockedLog) Write(p []byte) (int, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.buf.Write(p)
}
func (l *lockedLog) has(s string) bool {
l.mu.Lock()
defer l.mu.Unlock()
return strings.Contains(l.buf.String(), s)
}
// tail is the lines of the log that contain s, for a failure to name what the part said of it.
func (l *lockedLog) tail(s string) string {
l.mu.Lock()
defer l.mu.Unlock()
var out []string
for _, line := range strings.Split(l.buf.String(), "\n") {
if strings.Contains(line, s) {
out = append(out, line)
}
}
return strings.Join(out, "\n")
}