Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/ledger_fake_test.go
T
jschoubben c73641c117 CAT-K12: the health on the ledger names the source and the move's last check, and fails for what the ledger owes
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
2026-11-04 12:21:51 +01:00

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
}