Files
mesh-sdk/go/process/state_test.go
T
jschoubben 5bc1bf9f0a ADR 0627 §4: asks and asker leave mesh-sdk for operator-channel's client; the channel's benches stay as package channel
The gate's and the assigner's asks carry the router's ask as its JSON, so mesh-sdk names no type of a seat
defined whole; the ask's id, options, digest and an option's level are read by the gate, the assigner and the
teller through operatorchannelclient. ViewAnswerFits keeps the asking's check, from the key. The channel and
intake benches are not defined whole: their shapes move to go/channel (ADR 0625 §9).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:27:00 +01:00

354 lines
12 KiB
Go

package process
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/busretry"
)
// fakeEntry is a fakeKV's KeyValueEntry: only Key, Value and Revision are read by State.
type fakeEntry struct {
jetstream.KeyValueEntry
key string
value []byte
rev uint64
}
func (e fakeEntry) Key() string { return e.key }
func (e fakeEntry) Value() []byte { return e.value }
func (e fakeEntry) Revision() uint64 { return e.rev }
// fakeKV is an in-memory jetstream.KeyValue: embedding the nil interface satisfies every method this test
// never calls, so only Get, Create and Update need a body (1188/AC2's "a fake KV").
type fakeKV struct {
jetstream.KeyValue
mu sync.Mutex
values map[string][]byte
revs map[string]uint64
next uint64
// initial is what a Watch hands first, before its nil marker; watcher is the last one made.
initial []jetstream.KeyValueEntry
watcher *fakeWatcher
// withTTL says the last create of a key carried an option: its time to live.
withTTL map[string]bool
}
func newFakeKV() *fakeKV { return &fakeKV{values: map[string][]byte{}, revs: map[string]uint64{}} }
func (f *fakeKV) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
f.mu.Lock()
defer f.mu.Unlock()
v, ok := f.values[key]
if !ok {
return nil, jetstream.ErrKeyNotFound
}
return fakeEntry{key: key, value: v, rev: f.revs[key]}, nil
}
func (f *fakeKV) Create(_ context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.withTTL == nil {
f.withTTL = map[string]bool{}
}
f.withTTL[key] = len(opts) > 0
if _, ok := f.values[key]; ok {
return 0, jetstream.ErrKeyExists
}
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.revs[key] != revision {
return 0, jetstream.ErrKeyRevisionMismatch
}
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.next++
f.values[key], f.revs[key] = value, f.next
return f.next, nil
}
func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.values, key)
delete(f.revs, key)
return nil
}
func (f *fakeKV) Keys(_ context.Context, _ ...jetstream.WatchOpt) ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
if len(f.values) == 0 {
return nil, jetstream.ErrNoKeysFound
}
keys := make([]string, 0, len(f.values))
for k := range f.values {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.StringSlice(keys))) // as a bucket lists them: in no order the caller may rely on
return keys, nil
}
// fakeWatcher is a jetstream.KeyWatcher fed by a test.
type fakeWatcher struct {
jetstream.KeyWatcher
updates chan jetstream.KeyValueEntry
pattern string
}
func (w *fakeWatcher) Updates() <-chan jetstream.KeyValueEntry { return w.updates }
func (w *fakeWatcher) Stop() error { return nil }
func (f *fakeKV) Watch(_ context.Context, pattern string, _ ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
f.mu.Lock()
defer f.mu.Unlock()
w := &fakeWatcher{updates: make(chan jetstream.KeyValueEntry, 16), pattern: pattern}
f.watcher = w
for _, e := range f.initial {
w.updates <- e
}
w.updates <- nil
return w, nil
}
func stateOn(kv jetstream.KeyValue) State {
return State{Module: "mesh-mail", Name: "sends", kv: func(context.Context) (jetstream.KeyValue, error) { return kv, nil }}
}
// askerStore is the store operator-channel's client keeps its asks in (mesh-contracts operatorchannelclient.Store,
// whose own test holds process.State to it).
type askerStore interface {
Get(key string) (*stdio.StateEntry, error)
Create(key string, value any) (uint64, error)
Update(key string, value any, revision uint64) (uint64, error)
}
// 1188/AC2: given the process connection's state type, when used as operator-channel's client's Store, then it
// compiles and behaves as the interface says, against a fake KV.
func TestStateSatisfiesAskerStore(t *testing.T) {
var _ askerStore = State{}
s := stateOn(newFakeKV())
if e, err := s.Get("ask.1"); err != nil || e != nil {
t.Fatalf("Get of an absent key: %v, %v", e, err)
}
rev, err := s.Create("ask.1", map[string]any{"a": 1})
if err != nil {
t.Fatalf("Create: %v", err)
}
e, err := s.Get("ask.1")
if err != nil || e == nil || e.Revision != rev {
t.Fatalf("Get after Create: %+v, %v", e, err)
}
if _, err := s.Create("ask.1", map[string]any{"a": 2}); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("Create of an existing key: %v, want stdio.ErrChanged", err)
}
rev2, err := s.Update("ask.1", map[string]any{"a": 3}, rev)
if err != nil {
t.Fatalf("Update at the revision read: %v", err)
}
if _, err := s.Update("ask.1", map[string]any{"a": 4}, rev); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("Update at a stale revision: %v, want stdio.ErrChanged", err)
}
if rev2 == rev {
t.Fatal("Update did not advance the revision")
}
// 1952 S6 (ADR 0564 clauses 4, 7): a key created with its own time to live is created once, a second create
// refused as an act begun already; a renewal writes it whole with a fresh time to live; a time to live the bus
// cannot keep is refused.
kv := newFakeKV()
s = stateOn(kv)
if _, err := s.CreateTTL("settle.H3", map[string]any{"a": 1}, time.Minute); err != nil || !kv.withTTL["settle.H3"] {
t.Fatalf("CreateTTL: %v, with a time to live %v", err, kv.withTTL["settle.H3"])
}
if _, err := s.CreateTTL("settle.H3", map[string]any{"a": 2}, time.Minute); !errors.Is(err, stdio.ErrChanged) {
t.Fatalf("CreateTTL of a live key: %v, want stdio.ErrChanged", err)
}
kv.withTTL["settle.H3"] = false
if _, err := s.RenewTTL("settle.H3", map[string]any{"a": 3}, time.Minute); err != nil || !kv.withTTL["settle.H3"] {
t.Fatalf("RenewTTL: %v, with a time to live %v", err, kv.withTTL["settle.H3"])
}
if e, _ := s.Get("settle.H3"); e == nil || string(e.Value) != `{"a":3}` {
t.Fatalf("after RenewTTL the key reads %+v", e)
}
if _, err := s.CreateTTL("pulse.S1", 1, 10*time.Millisecond); err == nil || !strings.Contains(err.Error(), "shorter") {
t.Fatalf("a time to live under a second: %v", err)
}
}
// 1840/S1: given a state, when a key is put twice, then each Put returns a new revision and the key reads
// back the last value, whether or not it had one.
func TestStatePutWritesAKeyWhole(t *testing.T) {
s := stateOn(newFakeKV())
rev, err := s.Put("k", map[string]any{"a": 1})
if err != nil || rev == 0 {
t.Fatalf("Put of a new key: %d, %v", rev, err)
}
rev2, err := s.Put("k", map[string]any{"a": 2})
if err != nil || rev2 <= rev {
t.Fatalf("Put over a key: %d, %v, want a revision after %d", rev2, err, rev)
}
e, err := s.Get("k")
if err != nil || e == nil || e.Revision != rev2 || string(e.Value) != `{"a":2}` {
t.Fatalf("Get after Put: %+v, %v", e, err)
}
// Issue 774: a bus that answers nothing for a moment (a user-list reload) is waited out, not a lost write.
t.Cleanup(busretry.Waits(10*time.Second, time.Millisecond, 2*time.Millisecond))
kv, blackedOut := newFakeKV(), 2
dark := State{Module: "mesh-mail", Name: "sends", kv: func(context.Context) (jetstream.KeyValue, error) {
if blackedOut > 0 {
blackedOut--
return nil, jetstream.ErrJetStreamNotEnabled
}
return kv, nil
}}
if rev, err := dark.Put("k", 1); err != nil || rev == 0 || blackedOut != 0 {
t.Fatalf("a Put through a short blackout: %d, %v", rev, err)
}
}
// 1840/S1: given a state, when its keys are listed, then they come sorted, and none is an empty list, not an error.
func TestStateKeysAreSortedAndEmptyWhenNone(t *testing.T) {
s := stateOn(newFakeKV())
if keys, err := s.Keys(); err != nil || len(keys) != 0 {
t.Fatalf("Keys of an empty state: %v, %v", keys, err)
}
for _, k := range []string{"b", "c", "a"} {
if _, err := s.Put(k, 1); err != nil {
t.Fatal(err)
}
}
keys, err := s.Keys()
if err != nil || strings.Join(keys, ",") != "a,b,c" {
t.Fatalf("Keys: %v, %v, want a,b,c", keys, err)
}
}
type watchEntry struct {
fakeEntry
op jetstream.KeyValueOp
}
func (e watchEntry) Operation() jetstream.KeyValueOp { return e.op }
// 1840/S2: given a state with current values (one of them deleted), when it is watched, then the handler gets
// each current value marked Current and not the deleted one, Watch returns, and a later put and delete arrive
// in order unmarked.
func TestStateWatchHandsCurrentValuesThenChanges(t *testing.T) {
kv := newFakeKV()
kv.initial = []jetstream.KeyValueEntry{
watchEntry{fakeEntry{key: "a", value: []byte(`1`), rev: 1}, jetstream.KeyValuePut},
watchEntry{fakeEntry{key: "gone", rev: 2}, jetstream.KeyValueDelete},
watchEntry{fakeEntry{key: "b", value: []byte(`2`), rev: 3}, jetstream.KeyValuePut},
}
got := make(chan stdio.StateChange, 8)
s := stateOn(kv)
if err := s.Watch("", func(c stdio.StateChange) error { got <- c; return nil }); err != nil {
t.Fatal(err)
}
for _, want := range []stdio.StateChange{
{State: "sends", Key: "a", Op: "put", Value: []byte(`1`), Revision: 1, Current: true},
{State: "sends", Key: "b", Op: "put", Value: []byte(`2`), Revision: 3, Current: true},
} {
select {
case c := <-got:
if c.Key != want.Key || c.Op != want.Op || string(c.Value) != string(want.Value) || c.Revision != want.Revision || c.Current != want.Current || c.State != want.State {
t.Fatalf("got %+v, want %+v", c, want)
}
default:
t.Fatalf("Watch returned before handling %+v", want)
}
}
if len(got) != 0 {
t.Fatalf("a deleted key was handed: %+v", <-got)
}
kv.watcher.updates <- watchEntry{fakeEntry{key: "a", value: []byte(`3`), rev: 4}, jetstream.KeyValuePut}
kv.watcher.updates <- watchEntry{fakeEntry{key: "b", rev: 5}, jetstream.KeyValueDelete}
for _, want := range []stdio.StateChange{
{Key: "a", Op: "put", Revision: 4}, {Key: "b", Op: "delete", Revision: 5},
} {
select {
case c := <-got:
if c.Key != want.Key || c.Op != want.Op || c.Revision != want.Revision || c.Current {
t.Fatalf("got %+v, want %+v", c, want)
}
case <-time.After(5 * time.Second):
t.Fatalf("no change %+v", want)
}
}
}
// 1840/S2: a key narrowing reaches the bus as a subject pattern: `*` one name, `**` the rest, empty every key.
func TestStateWatchNarrowing(t *testing.T) {
for key, want := range map[string]string{"": ">", "a.b": "a.b", "a.*": "a.*", "a.**": "a.>", "**": ">"} {
if got := watchPattern(key); got != want {
t.Errorf("watchPattern(%q) = %q, want %q", key, got, want)
}
}
}
// 1840/S2: given a handler that refuses a current value, then Watch returns its error.
func TestStateWatchReturnsAHandlersErrorOnCurrentValues(t *testing.T) {
kv := newFakeKV()
kv.initial = []jetstream.KeyValueEntry{watchEntry{fakeEntry{key: "a", value: []byte(`1`), rev: 1}, jetstream.KeyValuePut}}
boom := errors.New("boom")
if err := stateOn(kv).Watch("", func(stdio.StateChange) error { return boom }); !errors.Is(err, boom) {
t.Fatalf("Watch: %v, want the handler's error", err)
}
}
// 1840/S2: given a connection that closes before the current values arrive, then Watch returns an error.
func TestStateWatchEndsWithAnErrorWhenTheConnectionCloses(t *testing.T) {
s := State{Module: "mesh-mail", Name: "sends", closed: func() bool { return true },
kv: func(context.Context) (jetstream.KeyValue, error) { return silentKV{}, nil }}
if err := s.Watch("", func(stdio.StateChange) error { return nil }); err == nil {
t.Fatal("Watch on a closed connection returned no error")
}
}
// silentKV watches but never delivers.
type silentKV struct{ jetstream.KeyValue }
func (silentKV) Watch(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
return &fakeWatcher{updates: make(chan jetstream.KeyValueEntry)}, nil
}
// 1840/S1: given a key, when it is deleted, then it reads as absent; deleting a key that was not there is no error.
func TestStateDeleteRemovesAKeyAndAnAbsentOneIsNoError(t *testing.T) {
s := stateOn(newFakeKV())
if _, err := s.Put("k", 1); err != nil {
t.Fatal(err)
}
if err := s.Delete("k"); err != nil {
t.Fatalf("Delete of a key: %v", err)
}
if e, err := s.Get("k"); err != nil || e != nil {
t.Fatalf("Get after Delete: %+v, %v, want absent", e, err)
}
if err := s.Delete("never"); err != nil {
t.Fatalf("Delete of an absent key: %v", err)
}
}