179 lines
7.8 KiB
Go
179 lines
7.8 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 travel on its binding, and a second binding carries none.
|
|
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",
|
|
Types: []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}}}}},
|
|
{Local: "untyped", 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{
|
|
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 ledger bound with no types is refused, naming it", `{"ledger":"untyped","instance":"decision/3","operation":"title",` + forged + `}`, "", `m's ledger "untyped" is bound with no types`},
|
|
{"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")
|
|
}
|
|
}
|