Unhealthy, naming the cause, for a ledger or the verdicts' stream that does not answer, a head behind its history, an index behind the ledger and a stream at its cap; the idle drafts, the parts waiting on the judge and the records the form-only check refused are counted and leave it healthy. A part waiting more than 30 minutes is asked of the operator once, cleared when it is judged. Issue 1966. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
269 lines
7.1 KiB
Go
269 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// fakeLedger plays the tool runner's side of the ledger for the module's own types (read from its manifest), as design
|
|
// 25 §12.4 says: apply checks an operation against the type, appends it with the sequence expected and derives the
|
|
// state by the operation's effect. It is a pipe, so a test of the store runs the same rules the manifest declares.
|
|
type fakeLedger struct {
|
|
t *testing.T
|
|
types map[string]catalogue.LedgerType
|
|
mu sync.Mutex
|
|
hist map[string][]buscalls.Operation
|
|
state map[string]map[string]any
|
|
// calls are the methods called, in order; applied are the call ids each apply named.
|
|
calls []string
|
|
applied []buscalls.ApplyRequest
|
|
// failApply, when set, refuses the apply of that operation.
|
|
failApply string
|
|
account string
|
|
// lag and fill are what lag and a stream's fill answer; noFill makes the stream not answer.
|
|
lag buscalls.Lagged
|
|
fill buscalls.StreamFilled
|
|
noFill bool
|
|
}
|
|
|
|
func newFakeLedger(t *testing.T) *fakeLedger {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := catalogue.Decode(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := &fakeLedger{t: t, types: map[string]catalogue.LedgerType{}, hist: map[string][]buscalls.Operation{}, state: map[string]map[string]any{}, account: "mesh-knowledge"}
|
|
for _, e := range m.BusState {
|
|
if e.Name == LedgerName {
|
|
for _, ty := range e.Types {
|
|
f.types[ty.Name] = ty
|
|
}
|
|
}
|
|
}
|
|
if len(f.types) != 2 {
|
|
t.Fatalf("the manifest's ledger has types %v", f.types)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f *fakeLedger) Call(method string, req, answer any) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, method)
|
|
raw, _ := json.Marshal(req)
|
|
var out any
|
|
switch method {
|
|
case buscalls.LedgerApply:
|
|
var r buscalls.ApplyRequest
|
|
_ = json.Unmarshal(raw, &r)
|
|
a, err := f.apply(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out = a
|
|
case buscalls.LedgerGet:
|
|
var r buscalls.InstanceRequest
|
|
_ = json.Unmarshal(raw, &r)
|
|
st := f.state[r.Instance]
|
|
is := buscalls.InstanceState{Instance: r.Instance, State: json.RawMessage("null")}
|
|
if st != nil {
|
|
b, _ := json.Marshal(st)
|
|
is.State, is.Sequence = b, uint64(len(f.hist[r.Instance]))
|
|
}
|
|
out = is
|
|
case buscalls.LedgerHistory:
|
|
var r buscalls.HistoryRequest
|
|
_ = json.Unmarshal(raw, &r)
|
|
ops := f.hist[r.Instance]
|
|
page := buscalls.HistoryPage{Last: uint64(len(ops))}
|
|
for _, o := range ops {
|
|
if o.Sequence < r.From {
|
|
continue
|
|
}
|
|
if r.Limit > 0 && len(page.Operations) == r.Limit {
|
|
page.Next = o.Sequence
|
|
break
|
|
}
|
|
page.Operations = append(page.Operations, o)
|
|
}
|
|
out = page
|
|
case buscalls.LedgerLag:
|
|
out = f.lag
|
|
case buscalls.StreamFill:
|
|
if f.noFill {
|
|
return fmt.Errorf("the stream does not answer")
|
|
}
|
|
out = f.fill
|
|
default:
|
|
return fmt.Errorf("the fake ledger answers no %s", method)
|
|
}
|
|
b, _ := json.Marshal(out)
|
|
return json.Unmarshal(b, answer)
|
|
}
|
|
|
|
func (f *fakeLedger) apply(r buscalls.ApplyRequest) (buscalls.Applied, error) {
|
|
if r.Ledger != LedgerName {
|
|
return buscalls.Applied{}, fmt.Errorf("no ledger %q", r.Ledger)
|
|
}
|
|
if f.failApply == r.Operation {
|
|
return buscalls.Applied{}, fmt.Errorf("refused: %s", r.Operation)
|
|
}
|
|
typeName, _, _ := strings.Cut(r.Instance, "/")
|
|
ty, ok := f.types[typeName]
|
|
if !ok {
|
|
return buscalls.Applied{}, fmt.Errorf("no type %q", typeName)
|
|
}
|
|
var op *catalogue.LedgerOperation
|
|
for i := range ty.Operations {
|
|
if ty.Operations[i].Name == r.Operation {
|
|
op = &ty.Operations[i]
|
|
}
|
|
}
|
|
if op == nil {
|
|
return buscalls.Applied{}, fmt.Errorf("the type %s allows no operation %q", typeName, r.Operation)
|
|
}
|
|
body := map[string]any{}
|
|
if len(r.Body) > 0 {
|
|
if err := json.Unmarshal(r.Body, &body); err != nil {
|
|
return buscalls.Applied{}, err
|
|
}
|
|
}
|
|
for name, field := range ty.Schema {
|
|
v, has := body[name]
|
|
if !has {
|
|
continue
|
|
}
|
|
okType := map[string]func(any) bool{
|
|
"string": func(v any) bool { _, ok := v.(string); return ok }, "integer": func(v any) bool { _, ok := v.(float64); return ok },
|
|
"object": func(v any) bool { _, ok := v.(map[string]any); return ok },
|
|
}[field.Type]
|
|
if okType != nil && !okType(v) {
|
|
return buscalls.Applied{}, fmt.Errorf("the field %s is not a %s", name, field.Type)
|
|
}
|
|
if field.DigestOf != "" {
|
|
if txt, _ := body[field.DigestOf].(string); v != SectionDigest(txt) {
|
|
return buscalls.Applied{}, fmt.Errorf("the %s is not the digest of its %s", name, field.DigestOf)
|
|
}
|
|
}
|
|
}
|
|
seq := uint64(len(f.hist[r.Instance]) + 1)
|
|
author := buscalls.Author{Account: f.account}
|
|
if r.Call != "" {
|
|
author.Caller = "caller-of-" + r.Call
|
|
}
|
|
f.hist[r.Instance] = append(f.hist[r.Instance], buscalls.Operation{Sequence: seq, Instance: r.Instance, Operation: r.Operation, Body: r.Body, Author: author, At: time.Unix(int64(seq), 0).UTC()})
|
|
f.applied = append(f.applied, r)
|
|
st := f.state[r.Instance]
|
|
if st == nil {
|
|
st = map[string]any{}
|
|
}
|
|
copied := map[string]any{}
|
|
for _, name := range op.Fields {
|
|
if v, ok := body[name]; ok {
|
|
copied[name] = v
|
|
}
|
|
}
|
|
if op.Sequence {
|
|
copied["sequence"] = seq
|
|
}
|
|
if op.Author {
|
|
copied["author"] = author
|
|
}
|
|
switch op.Effect {
|
|
case catalogue.EffectSetFields:
|
|
for k, v := range copied {
|
|
st[k] = v
|
|
}
|
|
case catalogue.EffectSetEntry:
|
|
m, _ := st[op.Into].(map[string]any)
|
|
if m == nil {
|
|
m = map[string]any{}
|
|
}
|
|
m[body[op.Key].(string)] = copied
|
|
st[op.Into] = m
|
|
default:
|
|
f.t.Fatalf("the fake ledger plays no effect %s", op.Effect)
|
|
}
|
|
f.state[r.Instance] = st
|
|
return buscalls.Applied{Taken: true, Sequence: seq}, nil
|
|
}
|
|
|
|
// fakeStates plays the module's states.
|
|
type fakeStates struct {
|
|
mu sync.Mutex
|
|
kv map[string]map[string][]byte
|
|
}
|
|
|
|
func newFakeStates() *fakeStates { return &fakeStates{kv: map[string]map[string][]byte{}} }
|
|
|
|
func (f *fakeStates) State(name string) KV { return fakeKV{f, name} }
|
|
|
|
type fakeKV struct {
|
|
f *fakeStates
|
|
name string
|
|
}
|
|
|
|
func (k fakeKV) bucket() map[string][]byte {
|
|
if k.f.kv[k.name] == nil {
|
|
k.f.kv[k.name] = map[string][]byte{}
|
|
}
|
|
return k.f.kv[k.name]
|
|
}
|
|
|
|
func (k fakeKV) Get(key string) (*stdio.StateEntry, error) {
|
|
k.f.mu.Lock()
|
|
defer k.f.mu.Unlock()
|
|
v, ok := k.bucket()[key]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return &stdio.StateEntry{Key: key, Value: v, Revision: 1}, nil
|
|
}
|
|
func (k fakeKV) Put(key string, value any) (uint64, error) {
|
|
k.f.mu.Lock()
|
|
defer k.f.mu.Unlock()
|
|
b, _ := json.Marshal(value)
|
|
k.bucket()[key] = b
|
|
return 1, nil
|
|
}
|
|
func (k fakeKV) Create(key string, value any) (uint64, error) {
|
|
k.f.mu.Lock()
|
|
defer k.f.mu.Unlock()
|
|
if _, ok := k.bucket()[key]; ok {
|
|
return 0, stdio.ErrChanged
|
|
}
|
|
b, _ := json.Marshal(value)
|
|
k.bucket()[key] = b
|
|
return 1, nil
|
|
}
|
|
func (k fakeKV) Update(key string, value any, _ uint64) (uint64, error) { return k.Put(key, value) }
|
|
func (k fakeKV) Delete(key string) error {
|
|
k.f.mu.Lock()
|
|
defer k.f.mu.Unlock()
|
|
delete(k.bucket(), key)
|
|
return nil
|
|
}
|
|
func (k fakeKV) Keys() ([]string, error) {
|
|
k.f.mu.Lock()
|
|
defer k.f.mu.Unlock()
|
|
var out []string
|
|
for key := range k.bucket() {
|
|
out = append(out, key)
|
|
}
|
|
return out, nil
|
|
}
|