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
354 lines
12 KiB
Go
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)
|
|
}
|
|
}
|