Merge branch 'feat/47-G-fix11-a5' into feat/47-G-join11
# Conflicts: # node-tools/internal/firstview/firstview_test.go # node-tools/internal/runtime/announce_test.go # node-tools/internal/runtime/busstate.go # node-tools/internal/runtime/ledgers_test.go
This commit is contained in:
@@ -2,6 +2,9 @@ package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -139,7 +142,60 @@ func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) {
|
||||
t.Fatalf("ping of the instance: %v", err)
|
||||
}
|
||||
var ping micro.Ping
|
||||
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "approval-mark/1,bus-state-calls,ledger-calls,put-sessions,seat-settle-delay" {
|
||||
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "approval-mark/1,bus-state-calls,finding-calls,ledger-calls,ledger-upkeep-calls,put-sessions,seat-settle-delay,sent-objects-calls" {
|
||||
t.Errorf("the runner states %q (%v), want its bus-state and ledger calls", ping.Metadata[announce.Capabilities], err)
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §462, §488, §622: each call is stated under the capability word of its own family, the words mesh-sdk's buscalls
|
||||
// names: the read of a sent object under sent-objects-calls (no longer under bus-state-calls), the ledger's fill and removal
|
||||
// under ledger-upkeep-calls, the others where they were.
|
||||
func TestEachCallIsStatedUnderTheWordOfItsFamily(t *testing.T) {
|
||||
for method, word := range map[string]string{
|
||||
buscalls.ObjectsSentGet: buscalls.SentObjectsCalls,
|
||||
buscalls.LedgerFill: buscalls.LedgerUpkeepCalls,
|
||||
buscalls.LedgerRemove: buscalls.LedgerUpkeepCalls,
|
||||
buscalls.LedgerApply: buscalls.LedgerCalls,
|
||||
buscalls.ObjectsPut: buscalls.BusStateCalls,
|
||||
} {
|
||||
var in []string
|
||||
for c, methods := range callFamilies {
|
||||
if slices.Contains(methods, method) {
|
||||
in = append(in, c)
|
||||
}
|
||||
}
|
||||
if len(in) != 1 || in[0] != word {
|
||||
t.Errorf("%s is stated under %v, want %s alone", method, in, word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §545: the words the tool runner states are exactly its manifest's artifact `states`, so the delivery plan's edge
|
||||
// of §605 runs to it for each word a module's artifact `calls`.
|
||||
func TestTheWordsTheRunnerStatesAreItsManifestsStates(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Build struct {
|
||||
Artifacts []struct {
|
||||
Name string `json:"name"`
|
||||
States []string `json:"states"`
|
||||
} `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var declared []string
|
||||
for _, a := range m.Build.Artifacts {
|
||||
if a.Name == "runtime" {
|
||||
declared = a.States
|
||||
}
|
||||
}
|
||||
slices.Sort(declared)
|
||||
if got := CallCapabilities(); !slices.Equal(got, declared) {
|
||||
t.Errorf("the runner states %v, its manifest's states are %v", got, declared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ import (
|
||||
// (announce.Capabilities) for its machine's node-engine to take into the node's record. A module whose manifest asks for one is
|
||||
// held off a machine whose runner does not state it.
|
||||
const (
|
||||
CapBusStateCalls = "bus-state-calls"
|
||||
CapLedgerCalls = "ledger-calls"
|
||||
CapBusStateCalls = buscalls.BusStateCalls
|
||||
CapLedgerCalls = buscalls.LedgerCalls
|
||||
// CapLedgerUpkeepCalls is the ledger's fill and removal (§462, §488) and CapSentObjectsCalls the read of a sent object (§488):
|
||||
// each is stated once the runner answers its family, the words mesh-sdk's buscalls names (§622).
|
||||
CapLedgerUpkeepCalls = buscalls.LedgerUpkeepCalls
|
||||
CapSentObjectsCalls = buscalls.SentObjectsCalls
|
||||
// CapPutSessions is the put and get sessions' word (design 47 G §518, §622), mesh-sdk's.
|
||||
CapPutSessions = stdio.CapPutSessions
|
||||
// CapApprovalMark says the runner honours the approval mark: its gate sits on every handler it serves (design 46
|
||||
@@ -30,16 +34,19 @@ const (
|
||||
// callFamilies are the methods each capability stands for, the ones BusState and BusStateWatch answer.
|
||||
var callFamilies = map[string][]string{
|
||||
CapBusStateCalls: {buscalls.ObjectsPut, buscalls.ObjectsGet, buscalls.ObjectsList, buscalls.ObjectsDelete, buscalls.ObjectsInfo,
|
||||
buscalls.ObjectsWatch, buscalls.ObjectsSentGet, buscalls.StreamRead, buscalls.StreamFill, buscalls.WorkQueueSize, buscalls.WorkQueueDead},
|
||||
buscalls.ObjectsWatch, buscalls.StreamRead, buscalls.StreamFill, buscalls.WorkQueueSize, buscalls.WorkQueueDead},
|
||||
CapLedgerCalls: {buscalls.LedgerApply, buscalls.LedgerGet, buscalls.LedgerHistory, buscalls.LedgerWatch, buscalls.LedgerQuery,
|
||||
buscalls.LedgerRebuild, buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag},
|
||||
CapPutSessions: {buscalls.ObjectsPutChunk, buscalls.ObjectsGetChunk},
|
||||
CapLedgerUpkeepCalls: {buscalls.LedgerFill, buscalls.LedgerRemove},
|
||||
CapSentObjectsCalls: {buscalls.ObjectsSentGet},
|
||||
CapPutSessions: {buscalls.ObjectsPutChunk, buscalls.ObjectsGetChunk},
|
||||
}
|
||||
|
||||
// CallCapabilities is every capability of the calls this runner answers, sorted. The delayed settle of a pulled seat
|
||||
// message names a fate, not a method, so it stands beside the method families (novox/hq ADR 0664 §5).
|
||||
// message names a fate, not a method, so it stands beside the method families (novox/hq ADR 0664 §5); so does `finding-calls`,
|
||||
// the acknowledged `mesh/publish` and the `mesh/subscribe` for new messages the launch answers (design 47 G §559).
|
||||
func CallCapabilities() []string {
|
||||
out := []string{stdio.CapSeatSettleDelay, CapApprovalMark}
|
||||
out := []string{stdio.CapSeatSettleDelay, stdio.CapFindingCalls, CapApprovalMark}
|
||||
for c := range callFamilies {
|
||||
out = append(out, c)
|
||||
}
|
||||
@@ -114,7 +121,7 @@ func (b *moduleBus) BusState(method string, params json.RawMessage) (json.RawMes
|
||||
answer, err = conn.WorkQueueDead(b.module, r)
|
||||
}
|
||||
case buscalls.LedgerApply, buscalls.LedgerGet, buscalls.LedgerHistory, buscalls.LedgerQuery, buscalls.LedgerRebuild,
|
||||
buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag:
|
||||
buscalls.LedgerReindex, buscalls.LedgerSnapshot, buscalls.LedgerLag, buscalls.LedgerFill, buscalls.LedgerRemove:
|
||||
answer, err = b.ledgerCall(method, params)
|
||||
default:
|
||||
return nil, fmt.Errorf("the runtime answers no %s", method)
|
||||
|
||||
@@ -240,6 +240,35 @@ func TestTheLedgerKeepsItsPromisesOnTheBus(t *testing.T) {
|
||||
t.Fatalf("queried %+v, %v", q, err)
|
||||
}
|
||||
}},
|
||||
{"fill and removal are answered through the calls and remove an instance from the bus whole", "decision.gone", func(t *testing.T) {
|
||||
must(t, open(t), "decision/gone", "title", `{"title":"Gone"}`)
|
||||
must(t, open(t), "decision/gone", "note", `{"says":"two"}`)
|
||||
must(t, open(t), "decision/stays", "note", `{"says":"kept"}`)
|
||||
raw, err := m.BusState(buscalls.LedgerFill, json.RawMessage(`{"ledger":"records"}`))
|
||||
var fill buscalls.Fill
|
||||
if _ = json.Unmarshal(raw, &fill); err != nil || fill.Count < 3 || fill.Bytes == 0 {
|
||||
t.Fatalf("the fill answered %s (%v)", raw, err)
|
||||
}
|
||||
raw, err = m.BusState(buscalls.LedgerRemove, json.RawMessage(`{"ledger":"records","instance":"decision/gone"}`))
|
||||
var removed buscalls.LedgerRemoved
|
||||
if _ = json.Unmarshal(raw, &removed); err != nil || removed.Operations != 2 || removed.Bytes == 0 {
|
||||
t.Fatalf("the removal answered %s (%v)", raw, err)
|
||||
}
|
||||
l := open(t)
|
||||
if last, _ := l.History.Last("decision.gone"); last != 0 {
|
||||
t.Fatalf("operations are left, at %d", last)
|
||||
}
|
||||
if st, _, _ := l.State.Get("decision.gone"); st != nil {
|
||||
t.Fatalf("the state is left: %s", st)
|
||||
}
|
||||
if last, _ := l.History.Last("decision.stays"); last == 0 {
|
||||
t.Fatal("another instance lost its operations")
|
||||
}
|
||||
if raw, err = m.BusState(buscalls.LedgerRemove, json.RawMessage(`{"ledger":"records","instance":"decision/gone"}`)); err != nil ||
|
||||
!strings.Contains(string(raw), `"operations":0`) {
|
||||
t.Fatalf("a removal said twice answered %s (%v)", raw, err)
|
||||
}
|
||||
}},
|
||||
{"a forged author is refused", "decision.forged", func(t *testing.T) {
|
||||
_, err := m.BusState(buscalls.LedgerApply, json.RawMessage(`{"ledger":"records","instance":"decision/forged","operation":"title","body":{"title":"T","author":"op@shanks"},"call":"forged"}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "the call forged, which is not a tool call in flight") {
|
||||
|
||||
@@ -171,6 +171,17 @@ func (b *moduleBus) ledgerCall(method string, params json.RawMessage) (any, erro
|
||||
var r buscalls.LedgerRequest
|
||||
_ = json.Unmarshal(params, &r)
|
||||
return l.Lag(r)
|
||||
case buscalls.LedgerFill:
|
||||
var r buscalls.LedgerRequest
|
||||
_ = json.Unmarshal(params, &r)
|
||||
return l.Fill(r)
|
||||
case buscalls.LedgerRemove:
|
||||
// Never on an apply: a removal is no operation of the history and has no author (design 47 G §587).
|
||||
var r buscalls.LedgerRemoveRequest
|
||||
if err := decode(method, params, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.Remove(r)
|
||||
}
|
||||
return nil, fmt.Errorf("the runtime answers no %s", method)
|
||||
}
|
||||
|
||||
@@ -205,13 +205,9 @@ func TestABundleToolNamedAsALedgerRepairIsRefused(t *testing.T) {
|
||||
// no other name; none answers "the runtime answers no ...".
|
||||
func TestEveryCallOfAStatedCapabilityIsAnswered(t *testing.T) {
|
||||
m, _, _ := aLedger(t)
|
||||
if got := strings.Join(CallCapabilities(), ","); got != "approval-mark/1,bus-state-calls,ledger-calls,put-sessions,seat-settle-delay" {
|
||||
if got := strings.Join(CallCapabilities(), ","); got != "approval-mark/1,bus-state-calls,finding-calls,ledger-calls,ledger-upkeep-calls,put-sessions,seat-settle-delay,sent-objects-calls" {
|
||||
t.Fatalf("the runner states %s", got)
|
||||
}
|
||||
// Design 47 G §462: the call that reads a sent object is listed under the bus state capability.
|
||||
if !slices.Contains(callFamilies[CapBusStateCalls], buscalls.ObjectsSentGet) {
|
||||
t.Errorf("%s does not list %s", CapBusStateCalls, buscalls.ObjectsSentGet)
|
||||
}
|
||||
for capability, methods := range callFamilies {
|
||||
for _, method := range methods {
|
||||
t.Run(capability+" "+method, func(t *testing.T) {
|
||||
|
||||
@@ -38,6 +38,26 @@ func TestNoBusWordReachesABundle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// design 47 G §618, §519: the runtime's own fingerprint reaches no bundle; the fingerprint a module's composed words hold does.
|
||||
func TestTheRuntimesOwnFingerprintReachesNoBundle(t *testing.T) {
|
||||
base := []string{"MESH_FINGERPRINT=runner-build", "PATH=/usr/bin"}
|
||||
for name, c := range map[string]struct {
|
||||
given map[string]string
|
||||
want string
|
||||
}{
|
||||
"composed with none sees none": {nil, ""},
|
||||
"composed with one sees its own": {map[string]string{"MESH_FINGERPRINT": "module-build"}, "MESH_FINGERPRINT=module-build"},
|
||||
} {
|
||||
env := strings.Join(BundleEnv(base, c.given, "telegram", "anchor"), "\n")
|
||||
if strings.Contains(env, "runner-build") {
|
||||
t.Errorf("%s: the runtime's own fingerprint reached the bundle: %s", name, env)
|
||||
}
|
||||
if c.want == "" && strings.Contains(env, "MESH_FINGERPRINT") || !strings.Contains(env, c.want) {
|
||||
t.Errorf("%s: the environment is %s", name, env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A tool call reaches only a tool's subject: whatever key a caller names, it is never a seat's event, accept
|
||||
// or proof — so no tool call says a choice, a link or a code, or submits an ask, in anybody's name.
|
||||
func TestAToolCallNeverReachesASeatsEventAcceptOrProof(t *testing.T) {
|
||||
|
||||
@@ -62,6 +62,10 @@ type ListedTool struct {
|
||||
// NodeRuntimeModule is the module that is a machine's runtime, serving every module on it.
|
||||
const NodeRuntimeModule = "node-tools"
|
||||
|
||||
// fingerprintEnv is the word the node-engine starts each process with, the fingerprint of the want it applies (mesh-sdk's
|
||||
// process.FingerprintEnv; this package imports no SDK): the runtime's own is no bundle's.
|
||||
const fingerprintEnv = "MESH_FINGERPRINT"
|
||||
|
||||
// runtimeOnly are the words of the runtime's own environment no bundle is given.
|
||||
var runtimeOnly = map[string]bool{ToolEnv: true, ToolModules: true, "MESH_BROKER_FILE": true,
|
||||
"MESH_BROKER_URL": true, "MESH_CONSOLE_LISTEN": true, LedgersFile: true}
|
||||
@@ -902,6 +906,21 @@ func (b *moduleBus) Publish(params json.RawMessage) error {
|
||||
return b.all.conn.PublishAs(b.module, env)
|
||||
}
|
||||
|
||||
// PublishAcked is Publish that answers the sequence the bus acknowledged the event at (`mesh/publish` with `ack`, design 47 G §559).
|
||||
func (b *moduleBus) PublishAcked(params json.RawMessage) (uint64, error) {
|
||||
var env bus.Envelope
|
||||
if err := json.Unmarshal(params, &env); err != nil {
|
||||
return 0, fmt.Errorf("not an event envelope: %w", err)
|
||||
}
|
||||
return b.all.conn.PublishAsAcked(b.module, env)
|
||||
}
|
||||
|
||||
// SubscribeNew hands the bundle one event's messages published from now on, on an ordered consumer of its own (`mesh/subscribe`
|
||||
// with `deliver: new`, design 47 G §559); the consumer ends with the bundle's channel.
|
||||
func (b *moduleBus) SubscribeNew(pattern string, deliver func(json.RawMessage) error) (func(), error) {
|
||||
return b.all.conn.SubscribeNew(pattern, deliver)
|
||||
}
|
||||
|
||||
// Ask calls a tool as the module: `{key, body}`, answered with the tool's result (ADR 0198).
|
||||
func (b *moduleBus) Ask(params json.RawMessage) (json.RawMessage, error) {
|
||||
var asked struct {
|
||||
@@ -1399,11 +1418,13 @@ func (b *moduleBus) bind(key string, stop func()) {
|
||||
// BundleEnv is what one module's bundle is started with: the runtime's environment without its own words,
|
||||
// the module's composed words over it, and the module's and machine's names. **No bus word reaches a
|
||||
// bundle** (security review of 2026-10-08): the credential and the bus's address are the runtime's, and a
|
||||
// bundle reaches the bus only through the runtime.
|
||||
// bundle reaches the bus only through the runtime. **The runtime's own fingerprint reaches none either**: a bundle reads
|
||||
// only the `MESH_FINGERPRINT` its module's composed words hold, the build the node-engine applied for that module, and with
|
||||
// none it has none (design 47 G §519, §618).
|
||||
func BundleEnv(base []string, given map[string]string, module, node string) []string {
|
||||
words := map[string]string{}
|
||||
for _, kv := range base {
|
||||
if k, v, ok := strings.Cut(kv, "="); ok && !runtimeOnly[k] {
|
||||
if k, v, ok := strings.Cut(kv, "="); ok && !runtimeOnly[k] && k != fingerprintEnv {
|
||||
words[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build integration
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// Design 47 G §559, A-153: against the real bus a publish with ack answers the sequence the bus gave it, and a subscription
|
||||
// for new messages hears only what is published after it was made, on the subject mesh-sdk's one rule names for the event;
|
||||
// a wildcard is refused.
|
||||
func TestAnAckedPublishAnswersItsSequenceAndANewSubscriptionHearsOnlyWhatComesAfter(t *testing.T) {
|
||||
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()
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEATEVENTS", Subjects: []string{"mesh.seat.mesh-conditions.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn := connect(t, "node-tools", "anchor")
|
||||
publish := func(body string) {
|
||||
t.Helper()
|
||||
if _, err := js.Publish(ctx, "mesh.seat.mesh-conditions.event.findings-asked", []byte(body)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
publish(`{"before":true}`)
|
||||
|
||||
var mu sync.Mutex
|
||||
var heard []string
|
||||
stop, err := conn.SubscribeNew("mesh-conditions.findings-asked", func(raw json.RawMessage) error {
|
||||
var env bus.Envelope
|
||||
_ = json.Unmarshal(raw, &env)
|
||||
mu.Lock()
|
||||
heard = append(heard, env.Key+" "+string(env.Body))
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
publish(`{"after":true}`)
|
||||
publish(`{"after":2}`)
|
||||
mt.Until(t, func() error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(heard) < 2 {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
return nil
|
||||
})
|
||||
mu.Lock()
|
||||
if len(heard) != 2 || heard[0] != `mesh-conditions.findings-asked {"after":true}` {
|
||||
t.Errorf("heard %v, want the messages after the subscription alone, in order", heard)
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
for name, want := range map[string]string{"mesh-conditions.*": "[A-Za-z0-9_-]"} {
|
||||
if _, err := conn.SubscribeNew(name, func(json.RawMessage) error { return nil }); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("a subscription for %s answered %v, want one naming %q", name, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
first, err := conn.PublishAsAcked("node-tools", bus.Envelope{Key: "finding-written", Body: json.RawMessage(`{}`)})
|
||||
second, err2 := conn.PublishAsAcked("node-tools", bus.Envelope{Key: "finding-written", Body: json.RawMessage(`{}`)})
|
||||
if err != nil || err2 != nil || first == 0 || second != first+1 {
|
||||
t.Errorf("acknowledged publishes answered %d (%v) and %d (%v), want consecutive sequences", first, err, second, err2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user