Apply a ledger operation: check it against its type, append it with the last sequence expected, derive the state at the revision read, write the snapshot (issue 1966 T2)

This commit is contained in:
2026-11-03 13:40:45 +01:00
parent d6c516ab44
commit ad22c5e9fe
5 changed files with 1042 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
package ledger
import (
"encoding/json"
"fmt"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
var owner = buscalls.Author{Account: "ledger.anchor.m"}
// 1966/AC13, 1966/AC14 and the checks of an operation against its type (ADR 0597 §2, §5, §6, §8; design 25 § 12.4).
func TestApplyAppendsDerivesAndSnapshots(t *testing.T) {
cases := []struct {
name string
run func(t *testing.T, l *Ledger, f fakes)
}{
{"1966/AC13 an instance at sequence 4 is taken with 5, its state and snapshot the effect's result", func(t *testing.T, l *Ledger, f fakes) {
for i := 1; i <= 4; i++ {
applied(t, l, "decision/0290", "note", fmt.Sprintf(`{"says":"n%d"}`, i), owner)
}
if last, _ := f.history.Last("decision.0290"); last != 4 {
t.Fatalf("the instance is at %d, want 4", last)
}
got := applied(t, l, "decision/0290", "title", `{"title":"A ledger","ignored":"x"}`, owner)
if !got.Taken || got.Sequence != 5 || got.StateBehind {
t.Fatalf("answered %+v, want taken with 5", got)
}
raw, _, _ := f.state.Get("decision.0290")
state := record(t, raw)
want := `{"author":{"account":"ledger.anchor.m"},"notes":[{"says":"n1"},{"says":"n2"},{"says":"n3"},{"says":"n4"}],"sequence":5,"title":"A ledger"}`
if state.Sequence != 5 || string(state.State) != want {
t.Fatalf("the state is %d %s, want 5 %s", state.Sequence, state.State, want)
}
snap, _, _ := f.snapshots.Get("decision.0290")
if string(snap) != string(raw) {
t.Fatalf("the snapshot is %s, the state %s", snap, raw)
}
}},
{"1966/AC14 a full state bucket answers taken and state behind, and the next apply after room catches up", func(t *testing.T, l *Ledger, f fakes) {
applied(t, l, "decision/0290", "note", `{"says":"first"}`, owner)
f.state.Full = true
got := applied(t, l, "decision/0290", "note", `{"says":"while full"}`, owner)
if !got.Taken || got.Sequence != 2 || !got.StateBehind {
t.Fatalf("answered %+v, want taken with 2 and state behind", got)
}
if raw, _, _ := f.state.Get("decision.0290"); record(t, raw).Sequence != 1 {
t.Fatalf("the full state moved: %s", raw)
}
f.state.Full = false
got = applied(t, l, "decision/0290", "note", `{"says":"after room"}`, owner)
raw, _, _ := f.state.Get("decision.0290")
state := record(t, raw)
if got.StateBehind || state.Sequence != 3 || string(state.State) != `{"notes":[{"says":"first"},{"says":"while full"},{"says":"after room"}]}` {
t.Fatalf("after room: %+v, the state %d %s", got, state.Sequence, state.State)
}
}},
{"each effect: set an entry, remove it, remove the instance", func(t *testing.T, l *Ledger, f fakes) {
text := "the section's text"
applied(t, l, "decision/1", "section", fmt.Sprintf(`{"id":"a","text":%q,"digest":%q}`, text, Digest(text)), owner)
applied(t, l, "decision/1", "section", fmt.Sprintf(`{"id":"b","text":"b","digest":%q}`, Digest("b")), owner)
applied(t, l, "decision/1", "drop-section", `{"id":"a"}`, owner)
raw, _, _ := f.state.Get("decision.1")
if got := string(record(t, raw).State); got != fmt.Sprintf(`{"sections":{"b":{"digest":%q}}}`, Digest("b")) {
t.Fatalf("the state is %s", got)
}
applied(t, l, "decision/1", "withdraw", `{}`, owner)
raw, _, _ = f.state.Get("decision.1")
if got := record(t, raw); got.Sequence != 4 || string(got.State) != "null" {
t.Fatalf("the removed instance is %d %s", got.Sequence, got.State)
}
}},
{"an operation outside its type, a body off its schema and a digest off its text are refused", func(t *testing.T, l *Ledger, f fakes) {
for _, c := range []struct{ instance, operation, body, refusal string }{
{"listing/a17", "title", `{}`, `no type "listing"`},
{"decision/0290", "accept", `{}`, `no operation "accept"`},
{"decision/0290", "title", `{"title":7}`, "field title is a string"},
{"decision/0290", "section", `{"id":"a","text":"x","digest":"sha256:00"}`, "is not the digest of its text"},
{"decision/0290", "section", `{"text":"x"}`, "is not the digest"},
{"decision/0290", "drop-section", `{}`, "names its entry by its field id"},
{"decision", "title", `{}`, "is not an instance"},
{"decision/a.b", "title", `{}`, "is not an instance"},
} {
_, err := l.Apply(buscalls.ApplyRequest{Instance: c.instance, Operation: c.operation, Body: json.RawMessage(c.body)}, owner)
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Errorf("%s %s %s: %v, want %q", c.instance, c.operation, c.body, err, c.refusal)
}
}
if last, _ := f.history.Last("decision.0290"); last != 0 {
t.Fatalf("a refused operation was appended: %d", last)
}
}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
l, f := newLedger()
c.run(t, l, f)
})
}
}