Files
mesh-tools/node-tools/internal/runtime/ledgers_test.go
T

178 lines
7.5 KiB
Go

package runtime
import (
"context"
"encoding/json"
"errors"
"sort"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/membership"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// 1966/AC16: given a module with a ledger binding, when the runner lists its tools, then the four repair tools are
// among them, marked acts or reads; a module without one has none, and a binding written later brings them.
func TestTheLedgerRepairsAreToolsOfEveryLedgerOwner(t *testing.T) {
mesh := mt.New(t)
owner := mt.MembershipOf("owner", "anchor", true, nil)
owner.Bindings = []membership.BusBinding{{Local: "records", Storage: catalogue.StorageLedger, Template: catalogue.TemplateLedger,
History: "LEDGER_owner_records", Subject: "mesh.ledger.owner.records", State: "owner_records",
Snapshot: "mesh.ledger-snapshot.owner.records"}}
plain := mt.MembershipOf("plain", "anchor", true, nil)
mesh.Issue(t, owner)
mesh.Issue(t, plain)
nodeTools := connect(t, "node-tools", "anchor")
stop, err := Run(nodeTools, []Served{{"owner", []string{mt.Fixture("caller-echo.py")}},
{"plain", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {})
if err != nil {
t.Fatal(err)
}
defer stop()
asker := connect(t, "console", "workstation")
listed := func(module string) (string, error) {
answered, err := asker.Ask(module+"."+ToolsVerb, map[string]any{}, "")
if err != nil {
return "", err
}
var got ToolsAnswer
if err := json.Unmarshal(answered.Result, &got); err != nil {
return "", err
}
var names []string
for _, tool := range got.Tools {
names = append(names, tool.Name+":"+tool.Effect)
}
sort.Strings(names)
return strings.Join(names, " "), nil
}
for _, c := range []struct{ name, module, want string }{
{"a ledger owner lists the four repairs", "owner", "ledger_lag:reads ledger_rebuild:acts ledger_reindex:acts ledger_snapshot:acts who:"},
{"a module with no ledger has none", "plain", "who:"},
} {
t.Run(c.name, func(t *testing.T) {
var got string
mt.Until(t, func() (err error) { got, err = listed(c.module); return })
if got != c.want {
t.Fatalf("%s lists %q, want %q", c.module, got, c.want)
}
})
}
t.Run("a ledger bound later brings them", func(t *testing.T) {
plain.Bindings = owner.Bindings
mesh.Issue(t, plain)
mt.Until(t, func() error {
got, err := listed("plain")
if err == nil && !strings.Contains(got, "ledger_rebuild:acts") {
err = errors.New("listed " + got)
}
return err
})
})
}
// aLedger raises one ledger of module m on the bus as the bus module would create it, binds it in m's membership, and
// answers the runtime's buses for m and for other, its types given as a test gives them.
func aLedger(t *testing.T) (m, other *moduleBus, mesh *mt.Mesh) {
t.Helper()
mesh = mt.New(t)
nc, err := nats.Connect(mt.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(nc.Close)
js, _ := jetstream.New(nc)
ctx := context.Background()
for _, cfg := range []jetstream.StreamConfig{
{Name: "LEDGER_m_records", Subjects: []string{"mesh.ledger.m.records.>"}},
{Name: "SNAPSHOT_m_records", Subjects: []string{"mesh.ledger-snapshot.m.records.>"}, MaxMsgsPerSubject: 1},
} {
if _, err := js.CreateStream(ctx, cfg); err != nil {
t.Fatal(err)
}
}
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "m_records"}); err != nil {
t.Fatal(err)
}
mem := mt.MembershipOf("m", "anchor", false, nil)
mem.Bindings = []membership.BusBinding{{Local: "records", Storage: catalogue.StorageLedger, Template: catalogue.TemplateLedger,
History: "LEDGER_m_records", Subject: "mesh.ledger.m.records", State: "m_records", Snapshot: "mesh.ledger-snapshot.m.records"}}
mesh.Issue(t, mem)
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
conn := connect(t, "node-tools", "anchor")
conn.Follow("m")
conn.Follow("other")
mt.Until(t, func() error {
if conn.Membership("m") == nil || conn.Membership("other") == nil {
return errors.New("not issued yet")
}
return nil
})
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}, ledgers: ledgerSource{
types: func(string, string) ([]catalogue.LedgerType, error) {
return []catalogue.LedgerType{{Name: "decision", Schema: map[string]catalogue.LedgerField{"title": {Type: "string"}},
Operations: []catalogue.LedgerOperation{{Name: "title", Effect: catalogue.EffectSetFields, Fields: []string{"title"}, Author: true}}}}, nil
},
writer: func(string) (*bus.Conn, error) { return conn, nil },
}}
return all.forModule("m").(*moduleBus), all.forModule("other").(*moduleBus), mesh
}
// 1966/AC17, 1966/AC18: the author an apply is stamped with is the caller the runtime proved for the call in flight it
// names, whatever author the bundle wrote; another module's call, or one not in flight, is refused naming it (ADR 0597 §7).
func TestAnApplyIsStampedWithTheCallerTheRuntimeProved(t *testing.T) {
m, other, _ := aLedger(t)
call, end := m.Begin("op@shanks")
defer end()
theirs, endTheirs := other.Begin("op@shanks")
defer endTheirs()
forged := `"body":{"title":"T","author":{"account":"x","caller":"forged"}}`
for _, c := range []struct{ name, params, caller, refusal string }{
{"1966/AC17 a call in flight stamps its proven caller", `{"ledger":"records","instance":"decision/1","operation":"title",` + forged + `,"call":"` + call + `","author":"forged"}`, "op@shanks", ""},
{"naming no call stamps the account alone", `{"ledger":"records","instance":"decision/2","operation":"title",` + forged + `}`, "", ""},
{"1966/AC18 another module's call is refused, naming it", `{"ledger":"records","instance":"decision/3","operation":"title",` + forged + `,"call":"` + theirs + `"}`, "", "the call " + theirs + ", which is another module's"},
{"a call not in flight is refused, naming it", `{"ledger":"records","instance":"decision/3","operation":"title",` + forged + `,"call":"c0ffee"}`, "", "the call c0ffee, which is not a tool call in flight"},
} {
t.Run(c.name, func(t *testing.T) {
var req struct {
Instance string `json:"instance"`
}
_ = json.Unmarshal([]byte(c.params), &req)
_, err := m.BusState(buscalls.LedgerApply, json.RawMessage(c.params))
if c.refusal != "" {
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Fatalf("applied with %v, want refused with %q", err, c.refusal)
}
return
}
if err != nil {
t.Fatal(err)
}
raw, err := m.BusState(buscalls.LedgerHistory, json.RawMessage(`{"ledger":"records","instance":"`+req.Instance+`"}`))
if err != nil {
t.Fatal(err)
}
var page buscalls.HistoryPage
_ = json.Unmarshal(raw, &page)
if len(page.Operations) != 1 || page.Operations[0].Author != (buscalls.Author{Account: "m", Caller: c.caller}) {
t.Fatalf("the history holds %+v, want the author {m %s}", page.Operations, c.caller)
}
state, _ := m.BusState(buscalls.LedgerGet, json.RawMessage(`{"ledger":"records","instance":"`+req.Instance+`"}`))
if !strings.Contains(string(state), `"author":{"account":"m"`) || strings.Contains(string(state), "forged") {
t.Fatalf("the state took the bundle's author: %s", state)
}
})
}
end()
if _, err := m.BusState(buscalls.LedgerApply, json.RawMessage(`{"ledger":"records","instance":"decision/4","operation":"title","call":"`+call+`"}`)); err == nil {
t.Fatal("a call that ended was still taken as in flight")
}
}