Files
jschoubben a196bf6323 Area 6 (join 11): every AnswerSeat caller follows a seat list and its grant
AnswerSeat checks a call's caller against the seat's definition its caller
reads from the seat list it follows (A-186). The bus module's keeper hands
it the mesh-broker definition its seat list holds once heard; mesh-recovery,
which followed none, follows one over its connection and an event hub, read
again once its events are live, and its manifest and its process invoke
seat-definitions and consume the list's two events. Its whole test answers
the seat list.

Builds: design 47 G lead answers A-179, A-186 (join 11 area 6)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 11:15:02 +01:00

661 lines
27 KiB
Go

//go:build integration
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"slices"
"strings"
"sync"
"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/seats/meshbroker"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
catalog "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-sdk/go/bussnap"
"nats-tools/internal/keeper"
)
// kvStore is the keeper's own state on the real bus: one bucket per state, as the grants part creates its old sections.
type kvStore struct {
js jetstream.JetStream
}
func (s kvStore) kv(state string) (jetstream.KeyValue, error) {
return s.js.KeyValue(context.Background(), "nats_"+state)
}
func (s kvStore) Get(state, key string) ([]byte, uint64, bool, error) {
kv, err := s.kv(state)
if err != nil {
return nil, 0, false, err
}
e, err := kv.Get(context.Background(), key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, 0, false, nil
}
if err != nil {
return nil, 0, false, err
}
return e.Value(), e.Revision(), true, nil
}
func (s kvStore) Put(state, key string, value []byte) (uint64, error) {
kv, err := s.kv(state)
if err != nil {
return 0, err
}
return kv.Put(context.Background(), key, value)
}
func (s kvStore) Delete(state, key string) error {
kv, err := s.kv(state)
if err != nil {
return err
}
return kv.Delete(context.Background(), key)
}
func (s kvStore) Create(state, key string, value []byte) (uint64, error) {
kv, err := s.kv(state)
if err != nil {
return 0, err
}
rev, err := kv.Create(context.Background(), key, value)
if errors.Is(err, jetstream.ErrKeyExists) {
return 0, keeper.ErrExists
}
return rev, err
}
func (s kvStore) Keys(state string) ([]string, error) {
kv, err := s.kv(state)
if err != nil {
return nil, err
}
keys, err := kv.Keys(context.Background())
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil
}
return keys, err
}
func eventually(t *testing.T, what string, ok func() bool) {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
for !ok() {
if time.Now().After(deadline) {
t.Fatalf("not within 10 s: %s", what)
}
time.Sleep(20 * time.Millisecond)
}
}
// The bus module runs whole (design 25 § 12.7, "The bus module runs whole" and "The bus module's first view"; CAT-N10):
// its keeper against the real bus server, planning from the catalog half of every watched branch and the seat list
// (design 47 G §344, §532, §533, §537, §538) at each move of them, creating on the bus,
// saying its events on its own stream BROKER_EVENTS, keeping its state in its own buckets, and acting on a person's
// requests. One server for every case, in order, as the mesh runs them.
func TestTheBusModuleRunsWhole(t *testing.T) {
dir := t.TempDir()
s, err := server.NewServer(&server.Options{Port: -1, JetStream: true, StoreDir: dir, NoLog: true, NoSigs: true})
if err != nil {
t.Fatal(err)
}
go s.Start()
if !s.ReadyForConnections(5 * time.Second) {
t.Fatal("the server did not start")
}
defer s.Shutdown()
nc, err := nats.Connect(s.ClientURL())
if err != nil {
t.Fatal(err)
}
defer nc.Close()
js, _ := jetstream.New(nc)
ctx := context.Background()
account := process.Account{Node: "anchor", Module: keeper.Module, User: "anchor.nats.keeper"}
// What the grants part creates for the bus module from its manifest's old sections: EVENTS and the keeper's
// consumer on it, its own stream, and its two states.
for _, c := range []jetstream.StreamConfig{
{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}},
{Name: BrokerEventsStream, Subjects: []string{"mesh.stream.nats.event.>"}, MaxAge: 30 * 24 * time.Hour, MaxBytes: 256 << 20},
{Name: "CONDITIONS", Subjects: []string{"mesh.seat.operator-channel.accept.condition.>"}},
} {
if _, err := js.CreateStream(ctx, c); err != nil {
t.Fatal(err)
}
}
// The keeper's consumer hears the indexer's verdicts and the operator channel's decided for the bus module's own
// asks, as the grants part composes it from its consumes and the seats it uses.
if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{Durable: account.QueueConsumerName(),
FilterSubjects: []string{DecidedSubject}, AckPolicy: jetstream.AckExplicitPolicy, MaxDeliver: 5}); err != nil {
t.Fatal(err)
}
// The router's own record of the asks, which the keeper reads under its own process account (process.Conn.Ask), never
// a stand-in: an ask's record is `<asker>.<ask id>`, as the router keeps it.
asksKV, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: asksBucket, History: 5})
if err != nil {
t.Fatal(err)
}
pconn, err := process.Connect(process.Account{URL: s.ClientURL(), Node: account.Node, Module: account.Module, User: account.User, Password: "p"})
if err != nil {
t.Fatal(err)
}
defer pconn.Close()
for _, st := range []string{keeper.StateAsked, keeper.StateRequests, keeper.StateSpent, keeper.StatePlans, "findings"} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "nats_" + st, History: 5}); err != nil {
t.Fatal(err)
}
}
store := kvStore{js}
snapshots := t.TempDir()
var mu sync.Mutex
// The catalog half as meshcatalogclient's branch follower holds it, branch by branch in the registrations' order, and
// the seat list's definitions; each change moves the keeper, as the follower's and the seat list's changes do.
branches := []Branch{{Name: "main", Revision: 1, Entries: map[string]catalog.CatalogEntry{}}}
order := []string{"main"}
defs := map[string]seatdef.Definition{}
moved := make(chan struct{}, 1)
move := func() {
select {
case moved <- struct{}{}:
default:
}
}
var acts []string
// The bus module's findings go through a held writer over its state findings, started as the keeper's process starts it.
writer, err := views.NewHeldWriter(views.WriterConfig[meshconditions.Finding, meshconditions.FindingCleared, meshconditions.FindingsStated, meshconditions.FindingsAskedEvent]{
Asked: meshconditions.SubjectFindingsAsked, Module: keeper.Module, Node: account.Node, State: pconn.State("findings"),
Kinds: []string{keeper.FindingSnapshotsFilling}, JS: js, Conn: nc, Logf: t.Logf})
if err != nil {
t.Fatal(err)
}
if err := writer.Start(ctx); err != nil {
t.Fatal(err)
}
defer writer.Close()
newKeeper := func() *keeper.Keeper {
return &keeper.Keeper{Bus: keeper.JetStream{JS: js, Snap: bussnap.Into(nc, snapshots), NC: nc, Dir: snapshots, Log: t.Logf}, Store: store,
Finding: writer, Emit: streamEmitter(js, account), Say: sayer(js),
Catalog: func() (keeper.CatalogView, error) {
mu.Lock()
defer mu.Unlock()
return ViewOf(branches, func(string) []string { return order })
},
Seats: func() map[string]seatdef.Definition { mu.Lock(); defer mu.Unlock(); return maps.Clone(defs) },
Again: func() error { return nil },
Act: func(what, why, cause string) error {
mu.Lock()
defer mu.Unlock()
acts = append(acts, cause+": "+what)
return nil
},
Warrant: WarrantFrom(pconn.Ask),
Logf: t.Logf}
}
run := func(k *keeper.Keeper) func() {
kctx, stop := context.WithCancel(ctx)
done := make(chan struct{})
watch := func(c context.Context, act func()) error {
kv, _ := js.KeyValue(ctx, "nats_"+keeper.StateRequests)
w, err := kv.WatchAll(c)
if err != nil {
return err
}
go func() {
for range w.Updates() {
act()
}
}()
return nil
}
go func() { _ = Keep(kctx, k, hearer(js, account, nil, nil, t.Logf), watch, moved, t.Logf); close(done) }()
return func() { stop(); <-done }
}
// onBranch puts a module's entry in a branch's newest revision (none: the module leaves it), and moves the keeper.
onBranch := func(branch, module string, revision int64, manifest map[string]any) {
mu.Lock()
for i := range branches {
if branches[i].Name == branch {
branches[i].Revision = revision
if manifest == nil {
delete(branches[i].Entries, module)
} else {
manifest["module"] = module
branches[i].Entries[module] = catalog.CatalogEntry{Module: module, Verdict: "accepted", ManifestDigest: fmt.Sprintf("sha256:%d", revision), Manifest: manifest}
}
}
}
mu.Unlock()
move()
}
accepted := func(module string, revision uint64, entries ...catalogue.BusStateEntry) {
onBranch("main", module, int64(revision), map[string]any{"bus-state": entries})
}
// saidOn reads what one stream holds of the bus module's events, by its subjects' prefix: its own stream (its
// history), or EVENTS on the seat mesh-broker's subjects (what every consumer of mesh-broker.<event> hears).
saidOn := func(stream, prefix string) []string {
cons, err := js.OrderedConsumer(ctx, stream, jetstream.OrderedConsumerConfig{FilterSubjects: []string{prefix + ">"}})
if err != nil {
t.Fatal(err)
}
info, _ := cons.Info(ctx)
var out []string
if info.NumPending == 0 {
return nil
}
batch, _ := cons.Fetch(int(info.NumPending), jetstream.FetchMaxWait(2*time.Second))
for m := range batch.Messages() {
var e keeper.Event
_ = json.Unmarshal(m.Data(), &e)
out = append(out, strings.TrimPrefix(m.Subject(), prefix)+" "+e.Module+" "+e.Local)
}
return out
}
said := func() []string { return saidOn(BrokerEventsStream, "mesh.stream.nats.event.") }
saidHas := func(want string) func() bool {
return func() bool {
for _, l := range said() {
if l == want {
return true
}
}
return false
}
}
held := func(stream string) (*jetstream.StreamInfo, bool) {
st, err := js.Stream(ctx, stream)
if err != nil {
return nil, false
}
info, err := st.Info(ctx)
return info, err == nil
}
state := entry("drafts", "key-value", "state", nil)
log := entry("log", "stream", "log", map[string]string{"max-mib": "1"})
// Review round 1 (running F1): the fill pass runs from the start, while the first view is still asked, so the store's
// one guard never waits on the indexer.
t.Run("the fill pass runs while the first view is asked", func(t *testing.T) {
down := newKeeper()
down.Catalog = func() (keeper.CatalogView, error) {
return keeper.CatalogView{}, errors.New("the catalog half has no first view of main yet")
}
stopDown := run(down)
eventually(t, "a fill pass with no first view", func() bool { return !down.Status().Pass.IsZero() && down.Status().FirstView.Answered.IsZero() })
stopDown()
// And while its consumer is bound again on its backoff (review round 2, running F1).
was := FillEvery
FillEvery = 20 * time.Millisecond
defer func() { FillEvery = was }()
deaf := newKeeper()
kctx, stopDeaf := context.WithCancel(ctx)
done := make(chan struct{})
go func() {
_ = Keep(kctx, deaf, func(context.Context, func(string, []byte) error) error { return errors.New("no consumer yet") }, nil, nil, t.Logf)
close(done)
}()
eventually(t, "the first view answered", func() bool { return !deaf.Status().FirstView.Answered.IsZero() })
first := deaf.Status().Pass
eventually(t, "a fill pass while the consumer is bound again", func() bool { return deaf.Status().Pass.After(first) })
stopDeaf()
<-done
})
k := newKeeper()
stop := run(k)
t.Run("a verdict, then created, with its binding", func(t *testing.T) {
accepted("m", 2, state, log)
eventually(t, "created m drafts and m log", func() bool { return saidHas("created m drafts")() && saidHas("created m log")() })
// Said on the seat's subject as well, on the shared events stream (the lead's ruling, 13:0x).
if seat := saidOn("EVENTS", "mesh.seat.mesh-broker.event."); !slices.Contains(seat, "created m drafts") ||
!slices.Contains(seat, "created m log") {
t.Errorf("EVENTS holds %v of mesh-broker's events: created m drafts and m log, as on the bus module's own stream", seat)
}
if _, ok := held("KV_m_drafts"); !ok {
t.Error("the bus holds no KV_m_drafts")
}
if info, ok := held("m_log"); !ok || info.Config.Discard != jetstream.DiscardNew || info.Config.Subjects[0] != "mesh.bus.m.log.>" {
t.Errorf("m_log is %+v: a log refuses when full, on the module's own subjects", info)
}
})
t.Run("a restart catches up a verdict given while it was down", func(t *testing.T) {
stop()
accepted("n", 3, entry("jobs", "stream", "work-queue", nil))
k = newKeeper()
stop = run(k)
eventually(t, "created n jobs", saidHas("created n jobs"))
})
t.Run("drift is said, a growth applied and the drift cleared", func(t *testing.T) {
accepted("m", 5, state, entry("log", "stream", "log", map[string]string{"max-mib": "1", "max-age-days": "2"}))
eventually(t, "drifted m log", saidHas("drifted m log"))
accepted("m", 6, state, entry("log", "stream", "log", map[string]string{"max-mib": "4"}))
eventually(t, "drift-cleared m log", saidHas("drift-cleared m log"))
if info, _ := held("m_log"); info.Config.MaxBytes != 4<<20 {
t.Errorf("m_log's cap is %d: the larger cap applied", info.Config.MaxBytes)
}
})
t.Run("a refused create hands no binding", func(t *testing.T) {
bare := entry("raw", "stream", "", nil)
accepted("m", 7, state, entry("log", "stream", "log", map[string]string{"max-mib": "4"}), bare)
eventually(t, "refused m raw", saidHas("refused m raw"))
if saidHas("created m raw")() {
t.Error("created m raw")
}
})
t.Run("each template at a small cap refuses, full raised at the raise mark and cleared below the clear mark", func(t *testing.T) {
for i := 0; ; i++ {
_, err := js.Publish(ctx, fmt.Sprintf("mesh.bus.m.log.k%d", i), make([]byte, 200<<10))
if err != nil {
break
}
if i > 40 {
t.Fatal("a 4 MiB log took more than 8 MiB")
}
}
k.FillPass()
eventually(t, "full m log", saidHas("full m log"))
st, _ := js.Stream(ctx, "m_log")
_ = st.Purge(ctx)
k.FillPass()
eventually(t, "full-cleared m log", saidHas("full-cleared m log"))
})
t.Run("an orphan is listed, and goes only by drop on the operator's warrant, after a snapshot", func(t *testing.T) {
accepted("m", 8, entry("log", "stream", "log", map[string]string{"max-mib": "4"}))
eventually(t, "orphaned m drafts", saidHas("orphaned m drafts"))
put := func(r keeper.Request) {
raw, _ := json.Marshal(r)
if _, err := store.Put(keeper.StateRequests, r.ID, raw); err != nil {
t.Fatal(err)
}
}
get := func(id string) keeper.Request {
raw, _, _, _ := store.Get(keeper.StateRequests, id)
var r keeper.Request
_ = json.Unmarshal(raw, &r)
return r
}
put(keeper.Request{ID: "r1", Verb: keeper.VerbDrop, Module: "m", Local: "drafts", Caller: "person.jo", Why: "unused"})
eventually(t, "the drop with no ask refused", func() bool { return strings.Contains(get("r1").Error, "operator's warrant") })
r := keeper.Request{ID: "r2", Verb: keeper.VerbDrop, Module: "m", Local: "drafts", Caller: "person.jo", Why: "unused", At: time.Now()}
a, _ := askFor(r, time.Now())
r.Ask = a.ID
record := func(w *oc.Warrant) {
raw, _ := json.Marshal(map[string]any{"asker": keeper.Module, "ask": a, "state": "open", "warrant": w})
if _, err := asksKV.Put(ctx, keeper.Module+"."+a.ID, raw); err != nil {
t.Fatal(err)
}
}
record(nil)
put(r)
eventually(t, "the request read, waiting for the operator", func() bool { return get("r2").Waiting != "" })
if _, ok := held("KV_m_drafts"); !ok || !get("r2").Done.IsZero() {
t.Fatal("dropped before the operator chose")
}
// The operator chooses: the router records the warrant and says decided; the keeper acts on decided, on its
// durable consumer, with no timer between and no call of its pass by the test (seams F1).
w := &oc.Warrant{Ask: a.ID, Asker: keeper.Module, Outcome: occ.OutcomeChosen, By: &oc.Person{Who: occ.Operator},
Option: "go", Label: "Remove", Level: occ.Destroy, At: occ.FormatTime(time.Now()), AskDigest: occ.Digest(a)}
record(w)
decided := time.Now()
body, _ := json.Marshal(w)
if _, err := js.Publish(ctx, DecidedSubject, body); err != nil {
t.Fatal(err)
}
eventually(t, "dropped m drafts", saidHas("dropped m drafts"))
if since := time.Since(decided); since > 5*time.Second {
t.Errorf("dropped %s after decided: within seconds", since)
}
if _, ok := held("KV_m_drafts"); ok {
t.Error("KV_m_drafts is still on the bus")
}
if p := get("r2").Snapshot; p == "" {
t.Error("no snapshot taken before the drop")
} else if st, err := os.Stat(p); err != nil || st.Size() == 0 {
t.Errorf("the snapshot %s is not there whole: %v", p, err)
}
mu.Lock()
recorded := strings.Join(acts, "\n")
mu.Unlock()
if !strings.Contains(recorded, "nats_drop: dropped drafts of m") {
t.Errorf("recorded %s: the drop as a hand act", recorded)
}
// The same warrant handed in a second request is refused and deletes nothing (security F2): drafts asked for and
// orphaned again, then a request naming the spent ask.
accepted("m", 9, state, entry("log", "stream", "log", map[string]string{"max-mib": "4"}))
eventually(t, "m drafts made again", func() bool { _, ok := held("KV_m_drafts"); return ok })
accepted("m", 10, entry("log", "stream", "log", map[string]string{"max-mib": "4"}))
eventually(t, "m drafts orphaned again", func() bool { return k.Held()["m"].Made["drafts"].Orphaned })
again := keeper.Request{ID: "r2b", Verb: keeper.VerbDrop, Module: "m", Local: "drafts", Caller: "person.jo", Why: "again", Ask: a.ID, At: time.Now()}
put(again)
eventually(t, "the second request on the spent warrant refused", func() bool { return strings.Contains(get("r2b").Error, "already acted") })
if _, ok := held("KV_m_drafts"); !ok {
t.Error("the spent warrant deleted KV_m_drafts a second time")
}
})
// Design 47 G §681, A-107: the snapshot the keeper wrote is sent by the keeper's own process (snapshot-get answered through
// AnswerSeat, the chunks published by the process and acknowledged to its own subscription), restored in place by the keeper
// (refused while its name is live, ended by snapshot-restored) and removed by snapshot-remove, no call handed through the requests.
t.Run("a snapshot is sent, restored in place and removed by the keeper's process, which answers the seat itself", func(t *testing.T) {
get := func(id string) keeper.Request {
raw, _, _, _ := store.Get(keeper.StateRequests, id)
var r keeper.Request
_ = json.Unmarshal(raw, &r)
return r
}
rec := get("r2")
if rec.Path == "" || rec.Digest == "" || rec.Protected {
t.Fatalf("the drop's record holds no snapshot: %+v", rec)
}
world := keeperSnapshots(nc, t.Logf)
// The seat's definition as the seat list holds it, the bootstrapped one (A-186).
broker := func() (seatdef.Definition, bool) {
for _, d := range seatlist.Bootstrapped() {
if d.Seat == meshbroker.Seat {
return d, true
}
}
return seatdef.Definition{}, false
}
stopAnswering, err := pconn.AnswerSeat(meshbroker.Seat, broker, snapshotAnswers(k, heldRequest(store), world))
if err != nil {
t.Fatal(err)
}
defer stopAnswering()
// A caller of the seat: its call carries its bus name as the last token, and it acknowledges each chunk it hears.
verb := func(name string, args map[string]any, into any) {
t.Helper()
raw, _ := json.Marshal(args)
msg, err := nc.Request("mesh.seat.mesh-broker.call."+name+".person~jo", raw, 10*time.Second)
if err != nil {
t.Fatal(err)
}
var reply struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
if err := json.Unmarshal(msg.Data, &reply); err != nil || reply.Error != "" {
t.Fatalf("%s answered %s (%v)", name, msg.Data, err)
}
if err := json.Unmarshal(reply.Result, into); err != nil {
t.Fatal(err)
}
}
call := "0123456789abcdef0123456789abcdef"
var chunkMu sync.Mutex
var chunks []Chunk
hear, err := nc.Subscribe("mesh.seat.mesh-broker.chunk.snapshot-get."+call+".*", func(m *nats.Msg) {
var c Chunk
_ = json.Unmarshal(m.Data, &c)
chunkMu.Lock()
chunks = append(chunks, c)
chunkMu.Unlock()
go func() {
_, _ = nc.Request(strings.Replace(m.Subject, ".chunk.", ".ack.", 1), []byte("{}"), 5*time.Second)
}()
})
if err != nil {
t.Fatal(err)
}
defer hear.Unsubscribe()
var got meshbroker.SnapshotGetAnswer
verb(meshbroker.VerbSnapshotGet, map[string]any{"request": "r2", "call": call}, &got)
if got.Error != "" || got.Digest != rec.Digest || got.Chunks < 1 || got.Stream != "m_drafts" {
t.Fatalf("snapshot-get answered %+v for the keeper's record %+v", got, rec)
}
var whole []byte
eventually(t, "the last chunk", func() bool {
chunkMu.Lock()
defer chunkMu.Unlock()
return len(chunks) > 0 && int64(len(chunks)) == chunks[0].Of
})
chunkMu.Lock()
for _, c := range chunks {
whole = append(whole, c.Data...)
}
chunkMu.Unlock()
if sum := sha256.Sum256(whole); "sha256:"+hex.EncodeToString(sum[:]) != rec.Digest || int64(len(whole)) != rec.Size {
t.Errorf("the chunks hold %d bytes, not the snapshot the keeper wrote (%d)", len(whole), rec.Size)
}
restore := func(why string) meshbroker.SnapshotRestoreAnswer {
var o meshbroker.SnapshotRestoreAnswer
verb(meshbroker.VerbSnapshotRestore, map[string]any{"request": "r2", "why": why}, &o)
return o
}
// KV_m_drafts is live (made again after the drop): nothing is restored over it.
if a := restore("live"); !strings.Contains(a.Error, "names live") {
t.Errorf("a restore over a live stream answered %+v", a)
}
// The state's snapshot holds its objects store beside it (design 47 G §58): a restore goes over neither.
for _, name := range []string{"KV_m_drafts", "OBJ_m_drafts-objects"} {
if err := js.DeleteStream(ctx, name); err != nil {
t.Fatal(err)
}
}
a := restore("it was removed by mistake")
if a.Accepted == "" {
t.Fatalf("restore answered %+v", a)
}
eventually(t, "snapshot-restored on the seat's subject", func() bool {
for _, l := range saidOn("EVENTS", "mesh.seat.mesh-broker.event.snapshot-restored.") {
if strings.HasPrefix(l, "r2."+a.Accepted+" ") {
return true
}
}
return false
})
if _, ok := held("KV_m_drafts"); !ok || get(a.Accepted).Result != "done" {
t.Errorf("the stream is not restored in place: held %v, the act %+v", ok, get(a.Accepted))
}
var o meshbroker.SnapshotRemoveAnswer
verb(meshbroker.VerbSnapshotRemove, map[string]any{"request": "r2", "why": "no longer needed"}, &o)
if o.Accepted == "" {
t.Fatalf("snapshot-remove answered %+v", o)
}
if _, err := os.Stat(rec.Path); !os.IsNotExist(err) {
t.Errorf("the snapshot file %s is still there: %v", rec.Path, err)
}
if _, _, found, _ := store.Get(keeper.StateRequests, "r2"); found {
t.Error("the snapshot's record outlived its file")
}
})
// §532: a branch registered while the keeper runs is followed with no restart; a feature branch asking a local name with
// other settings than the default branch's is said drifted, naming it, while the default branch's are planned, and its
// new local name is planned from it.
t.Run("a branch registered while it runs is planned from, a local name with other settings drifted", func(t *testing.T) {
mu.Lock()
branches = append(branches, Branch{Name: "feature", Revision: 4, Entries: map[string]catalog.CatalogEntry{"m": {Module: "m", Verdict: "accepted",
Manifest: map[string]any{"module": "m", "bus-state": []any{state, entry("log", "stream", "log", map[string]string{"max-mib": "2"}), entry("extra", "key-value", "state", nil)}}}}})
order = []string{"main", "feature"}
mu.Unlock()
move()
eventually(t, "created m extra", saidHas("created m extra"))
eventually(t, "drifted m log", saidHas("drifted m log"))
if info, _ := held("m_log"); info.Config.MaxBytes != 4<<20 {
t.Errorf("m_log's cap is %d: the default branch's 4 MiB planned", info.Config.MaxBytes)
}
mu.Lock()
branches, order = branches[:1], []string{"main"}
mu.Unlock()
move()
eventually(t, "orphaned m extra", saidHas("orphaned m extra"))
})
// §344, §533, §537: a module of the new form claiming a seat with a verb gets a sent store, one with neither none, a seat
// whose verbs come only from an included part one, a seat including only bodies none, a foundation part none; a
// definition gaining a verb gives its holder one. §538: a module in no watched branch's newest revision loses its plans key.
t.Run("the sent stores follow the catalog half and the seat list", func(t *testing.T) {
mu.Lock()
defs["verbed"] = seatdef.Definition{Seat: "verbed", Verbs: []seatdef.Verb{{Name: "go"}}}
defs["bare"] = seatdef.Definition{Seat: "bare"}
defs["resolved"] = seatdef.Definition{Seat: "resolved", Includes: []string{"part"}, Verbs: []seatdef.Verb{{Name: "from-the-part"}}}
defs["bodies"] = seatdef.Definition{Seat: "bodies", Includes: []string{"shared-bodies"}}
mu.Unlock()
claims := func(seat string) map[string]any { return map[string]any{"claims": []any{map[string]any{"name": seat}}} }
onBranch("main", "sa", 5, claims("verbed"))
onBranch("main", "sb", 5, claims("bare"))
onBranch("main", "sc", 5, claims("resolved"))
onBranch("main", "sd", 5, claims("bodies"))
f := claims("verbed")
f["objects"] = []any{map[string]any{"name": "sent-objects", "max-mib": 256}}
onBranch("main", "se", 5, f)
eventually(t, "sa and sc's sent stores", func() bool {
_, a := held("OBJ_sa_sent-objects")
_, c := held("OBJ_sc_sent-objects")
return a && c
})
for _, none := range []string{"sb", "sd", "se"} {
if _, ok := held("OBJ_" + none + "_sent-objects"); ok {
t.Errorf("%s has a sent store", none)
}
}
mu.Lock()
defs["bodies"] = seatdef.Definition{Seat: "bodies", Includes: []string{"shared-bodies"}, Verbs: []seatdef.Verb{{Name: "new"}}}
mu.Unlock()
move()
eventually(t, "sd's sent store once its seat gains a verb", func() bool { _, ok := held("OBJ_sd_sent-objects"); return ok })
onBranch("main", "sb", 6, nil)
eventually(t, "sb's plan deleted", func() bool { _, _, found, _ := store.Get(keeper.StatePlans, "sb"); return !found })
})
t.Run("nats_first_view_again asks at once and records the hand act", func(t *testing.T) {
raw, _ := json.Marshal(keeper.Request{ID: "r3", Verb: keeper.VerbFirstViewAgain, Why: "a verdict thought missed", Caller: "person.jo"})
_, _ = store.Put(keeper.StateRequests, "r3", raw)
eventually(t, "the first view asked again", func() bool {
mu.Lock()
defer mu.Unlock()
return strings.Contains(strings.Join(acts, "\n"), "nats_first_view_again")
})
})
stop()
}
func entry(name, storage, template string, options map[string]string) catalogue.BusStateEntry {
e := catalogue.BusStateEntry{Name: name, Storage: storage, Template: template}
if len(options) > 0 {
e.Options = map[string]json.RawMessage{}
for k, v := range options {
e.Options[k] = json.RawMessage(v)
}
}
return e
}