Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/ledgerstore_test.go
T
jschoubben 2770a55b61 Join item 2: mesh-knowledge's ledger pipe, call ids and page heartbeat come from the SDK
The ledger pipe asks the runtime through stdio.BusState (it answered ErrNoPipe before), an
operation names the call the runtime proved (stdio.CallIDOf), and the page takes the
runtime's word on its consumer (stdio.OnHeartbeat): a beat is every subscription heard, a
missed heartbeat marks each subscribed source at once with the runtime's why. Rows in
TestASourceThatDoesNotAnswerIsSaidAndAskedAgainOnABackoff and
TestTheLedgerStoreRefusesWhatTheLedgerWouldHaveToTakeBack; each failed before the change.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:33:35 +01:00

207 lines
8.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
func ledgerRecord(n int, status string, secs ...Section) *Record {
return &Record{Kind: KindResearch, Number: n, Title: "A record", Status: status, Date: "2026-11-04", Sections: secs}
}
// A record is one instance; a section's text is written once, in its own operation, and a new version names it by its
// digest and sequence; the head holds no text (to-be 55 §2.1, AC46).
func TestTheLedgerStoreKeepsEachSectionOnceAndTheHeadWithoutText(t *testing.T) {
ctx := context.Background()
f := newFakeLedger(t)
s := newLedgerStore(f, newFakeStates())
at := time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
if err := s.Create(ctx, Version{By: "a@n", At: at, Why: "proposed", Record: ledgerRecord(81, StatusProposed, Section{"Overview", "one"}, Section{"Direction", "two"})}); err != nil {
t.Fatal(err)
}
e, _ := s.Get(ctx, Key{KindResearch, 81})
r2 := e.Current.Clone()
r2.Sections[1].Text = "two, revised"
if err := s.Save(ctx, Version{By: "a@n", At: at, Why: "revised", Record: r2}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
// Three texts written (one, two, two revised), never "one" again; the operations are named by what they did.
var names []string
for _, a := range f.applied {
names = append(names, a.Operation)
}
if got := strings.Join(names, " "); got != "write-section write-section propose write-section submit" {
t.Fatalf("operations %q", got)
}
head := string(mustJSON(t, f.state["research/081"]["entry"]))
if strings.Contains(head, "two, revised") || strings.Contains(head, `"text"`) {
t.Fatalf("the head holds a section's text: %s", head)
}
got, _ := s.Get(ctx, Key{KindResearch, 81})
if got.Current.Sections[1].Text != "two, revised" || got.Current.Sections[0].Text != "one" || got.Versions != 2 || got.CurrentVersion != 2 {
t.Fatalf("%+v", got.Current.Sections)
}
// A reader that holds nothing (a restart) hydrates from the history, every version.
fresh := newLedgerStore(f, newFakeStates())
vs, err := fresh.Versions(ctx, Key{KindResearch, 81})
if err != nil || len(vs) != 2 || vs[0].Record.Sections[1].Text != "two" || vs[1].Record.Sections[1].Text != "two, revised" || vs[1].Why != "revised" {
t.Fatalf("%v %+v", err, vs)
}
}
func TestTheLedgerStoreRefusesWhatTheLedgerWouldHaveToTakeBack(t *testing.T) {
ctx := context.Background()
f := newFakeLedger(t)
s := newLedgerStore(f, newFakeStates())
v := Version{By: "a@n", At: time.Now(), Why: "x", Record: ledgerRecord(5, StatusProposed, Section{"Overview", "one"})}
if err := s.Create(withCall(ctx, "call-9"), v); err != nil {
t.Fatal(err)
}
// The tool call in flight is named to the ledger, so the author stamped is the caller it proved; a background act names none.
if last := f.applied[len(f.applied)-1]; last.Call != "call-9" || f.hist["research/005"][1].Author.Caller != "caller-of-call-9" {
t.Fatalf("%+v", last)
}
if err := s.Create(ctx, v); !errors.Is(err, ErrExists) {
t.Fatalf("a record held is created again: %v", err)
}
e, _ := s.Get(ctx, v.Record.Key())
if err := s.Save(ctx, v, true, false, e.Revision-1); !errors.Is(err, ErrChanged) {
t.Fatalf("a write on a head already moved: %v", err)
}
big := ledgerRecord(6, StatusProposed, Section{"Overview", strings.Repeat("x", MostSectionBytes+1)})
if err := s.Create(ctx, Version{Record: big}); err == nil || !strings.Contains(err.Error(), "split it") {
t.Fatalf("a section over its bound: %v", err)
}
if h, _ := s.Get(ctx, big.Key()); h != nil {
t.Fatal("a refused record is held")
}
// A text altered in the history is caught by its digest.
op := &f.hist["research/005"][0]
op.Body = mustJSON(t, map[string]any{"title": "Overview", "text": "altered", "digest": SectionDigest("one")})
if _, err := newLedgerStore(f, newFakeStates()).Get(ctx, v.Record.Key()); err == nil || !strings.Contains(err.Error(), "digest") {
t.Fatalf("a damaged history was read: %v", err)
}
// The ledger's refusal is the call's refusal.
f.failApply = OpPropose
if err := s.Create(ctx, Version{Record: ledgerRecord(7, StatusProposed)}); err == nil {
t.Fatal("a refused apply was taken")
}
// The bundle's own pipe is the runtime's call on its bus state (stdio.BusState), and the call it serves is the one
// the runtime named (stdio.CallIDOf): the method and request go through whole, the answer comes back decoded.
if reflect.ValueOf(busState).Pointer() != reflect.ValueOf(stdio.BusState).Pointer() ||
reflect.ValueOf(callIDOf).Pointer() != reflect.ValueOf(stdio.CallIDOf).Pointer() {
t.Fatal("the pipe is not the SDK's bus-state call, or the call id not the runtime's")
}
defer func(was func(string, any) (json.RawMessage, error)) { busState = was }(busState)
var asked string
busState = func(method string, params any) (json.RawMessage, error) {
asked = method
return json.RawMessage(`{"instance":"1"}`), nil
}
var got struct{ Instance string }
if err := (sdkPipe{}).Call(buscalls.LedgerGet, buscalls.InstanceRequest{Ledger: LedgerName, Instance: "1"}, &got); err != nil ||
asked != buscalls.LedgerGet || got.Instance != "1" {
t.Fatalf("the pipe asked %s and answered %+v, %v", asked, got, err)
}
}
func TestTheOperationAChangeIsIsNamedByWhatChanged(t *testing.T) {
base := ledgerRecord(1, StatusProposed)
base.Author = "a@n"
with := func(f func(*Record)) *Record { c := base.Clone(); f(c); return c }
held := &Entry{Current: base, CurrentVersion: 1, Versions: 1}
for _, c := range []struct {
name string
old *Entry
next *Record
want string
}{
{"a reserved number", nil, ledgerRecord(1, StatusReserved), OpReserve},
{"a proposal", nil, base, OpPropose},
{"an import", nil, with(func(r *Record) { r.Author, r.Source = "", "hq@abc:x.md" }), OpImport},
{"a revision", held, with(func(r *Record) { r.Title = "B" }), OpSubmit},
{"a review", held, with(func(r *Record) { r.Reviews = []Review{{By: "j"}} }), OpReview},
{"an acceptance", held, with(func(r *Record) { r.Status = StatusAccepted }), OpAccept},
{"a withdrawal", held, with(func(r *Record) { r.Status = StatusWithdrawn }), OpWithdraw},
{"an ask", held, with(func(r *Record) { r.Ask = &OperatorAsk{ID: "x", Said: "waiting"} }), OpAsk},
{"a warrant", &Entry{Current: with(func(r *Record) { r.Ask = &OperatorAsk{ID: "x", Said: "waiting"} })}, with(func(r *Record) { r.Ask = &OperatorAsk{ID: "x", Said: "approved", Warrant: "w"} }), OpWarrant},
{"a take-over", held, with(func(r *Record) { r.Author = "b@n" }), OpTakeOver},
{"a drafting step", held, with(func(r *Record) { r.Drafting = &Drafting{State: DraftWaiting} }), OpDraft},
{"a retraction asked", held, with(func(r *Record) { r.Retracting = &Retraction{Why: "x"} }), OpRetract},
} {
if got := operationFor(c.old, Version{Record: c.next}, true); got != c.want {
t.Errorf("%s: %s, want %s", c.name, got, c.want)
}
}
}
func TestTheFindingsAndTheToldMarksAreKeysOfTheState(t *testing.T) {
ctx := context.Background()
st := newFakeStates()
s := newLedgerStore(newFakeLedger(t), st)
if err := s.SetFindings(ctx, []Finding{{Rule: "cited", Says: "x"}}); err != nil {
t.Fatal(err)
}
if f, _ := s.Findings(ctx); len(f) != 1 || f[0].Rule != "cited" {
t.Fatalf("%v", f)
}
if first, _ := s.Told(ctx, "clauses.0290"); !first {
t.Fatal("told once")
}
if again, _ := s.Told(ctx, "clauses.0290"); again {
t.Fatal("told twice")
}
_, _ = s.Told(ctx, "review.0291")
if got, _ := s.ToldOf(ctx, "clauses."); len(got) != 1 || got[0] != "clauses.0290" {
t.Fatalf("%v", got)
}
_ = s.Untold(ctx, "clauses.0290")
if first, _ := s.Told(ctx, "clauses.0290"); !first {
t.Fatal("told again after it was over")
}
}
func mustJSON(t *testing.T, v any) json.RawMessage {
t.Helper()
b, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return b
}
var _ = buscalls.LedgerApply
// A restart finds every record the ledger holds, however far apart their numbers are: the ledger lists none, so the store
// keeps a key for each record it creates.
func TestARestartFindsEveryRecordHoweverSparseTheirNumbers(t *testing.T) {
f := newFakeLedger(t)
st := newFakeStates()
s := newLedgerStore(f, st)
for _, n := range []int{11, 127, 900} {
if err := s.Create(context.Background(), Version{By: "a@n", Record: ledgerRecord(n, StatusProposed, Section{"Overview", "x"})}); err != nil {
t.Fatal(err)
}
}
all, err := newLedgerStore(f, st).All(context.Background())
if err != nil || len(all) != 3 || all[2].Current.Number != 900 {
t.Fatalf("%v %d", err, len(all))
}
// A ledger written before the keys existed is probed, and the keys are written.
bare := newFakeStates()
if all, err := newLedgerStore(f, bare).All(context.Background()); err != nil || len(all) != 3 {
t.Fatalf("%v %d", err, len(all))
}
if keys, _ := bare.State(findingsState).Keys(); len(keys) != 3 {
t.Fatalf("%v", keys)
}
}