Round 2 T2, T3, T4, T5: the tool runner ends G with its merge-base test count; the wait test runs its rows side by side
The tests G added fold into existing ones as subtests (design 47 G §626, A-306: 171 tests down to 154, integration 9 down to 7). TestMeshWaitRefusesLoudlyAndNeverLoops runs its groups of one-second rows as parallel subtests, each on a runtime of its own, 15 s down to 5 s (A-311: the one-second floor of a wait's timeout stands, no clock is injected). The go.mod no longer requires meshconditionsclient, which no package loads (T4). Node names in the lines G adds are role names (T5). Builds: design 47 G §626, design 34 every-event §19, A-306, A-311; review round 2 findings T2, T3, T4, T5 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -2,7 +2,6 @@ package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -10,6 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/micro"
|
||||
|
||||
@@ -150,57 +151,55 @@ func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) {
|
||||
if ping.Metadata[announce.Fingerprint] != "sha256:runner" {
|
||||
t.Errorf("the runner states its fingerprint as %q", ping.Metadata[announce.Fingerprint])
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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.
|
||||
t.Run("EachCallIsStatedUnderTheWordOfItsFamily", func(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)
|
||||
}
|
||||
}
|
||||
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`.
|
||||
t.Run("TheWordsTheRunnerStatesAreItsManifestsStates", func(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
var m struct {
|
||||
Build struct {
|
||||
Artifacts []struct {
|
||||
Name string `json:"name"`
|
||||
States []string `json:"states"`
|
||||
} `json:"artifacts"`
|
||||
} `json:"build"`
|
||||
}
|
||||
}
|
||||
slices.Sort(declared)
|
||||
if got := CallCapabilities(); !slices.Equal(got, declared) {
|
||||
t.Errorf("the runner states %v, its manifest's states are %v", got, declared)
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/approval"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -121,9 +122,76 @@ func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) {
|
||||
{map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Class: seatdef.ClassAct}}}}, ""},
|
||||
} {
|
||||
defs := c.defs
|
||||
err := (&consumers{definitions: func() map[string]seatdef.Definition { return defs }}).admitted("node-desk", "who", "shanks.node-tools")
|
||||
err := (&consumers{definitions: func() map[string]seatdef.Definition { return defs }}).admitted("node-desk", "who", "workstation.node-tools")
|
||||
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) {
|
||||
t.Errorf("a proven caller with the definitions %v: %v, want %q", c.defs, err, c.says)
|
||||
}
|
||||
}
|
||||
// Design 47 G A-249: a bundle's seat call is received by the runner in chunks only for a verb whose definition says `chunks`,
|
||||
// of any machine: a verb a node holds is asked of the node that ran the ask (A-254).
|
||||
t.Run("OnlyAChunkedSeatVerbIsAskedInChunks", func(t *testing.T) {
|
||||
c := &consumers{definitions: func() map[string]seatdef.Definition {
|
||||
return map[string]seatdef.Definition{"mesh-catalog": {Verbs: []seatdef.Verb{{Name: "input", Chunks: true}, {Name: "entry"}}}}
|
||||
}}
|
||||
for key, want := range map[string]bool{
|
||||
"seat:mesh-catalog.input": true,
|
||||
"seat:mesh-catalog.entry": false,
|
||||
"seat:mesh-catalog.input@home-server": true,
|
||||
"seat:mesh-catalog.entry@home-server": false,
|
||||
"seat:mesh-unread.input": false,
|
||||
"mesh-delivery-indexer.indexer": false,
|
||||
} {
|
||||
if got := c.chunked(key); got != want {
|
||||
t.Errorf("chunked(%s) = %v, want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
if (&consumers{}).chunked("seat:mesh-catalog.input") {
|
||||
t.Error("a runner that holds no seat definitions asks no chunks")
|
||||
}
|
||||
})
|
||||
// **The tool runner answers a held seat's marked verb by caller and serves the seat's `approvals`** (design 47 G §677; lead
|
||||
// answers A-265, A-271), with zsh's node-login-shell `execute` as the case: the marked verb reaches the gate with the caller
|
||||
// its subject names, and `approvals` is answered from the runner's own record of the calls its gate holds, with the
|
||||
// arguments for the call's own caller only, though no bundle names the verb.
|
||||
t.Run("TheRunnerServesApprovalsForTheSeatsItsBundlesHold", func(t *testing.T) {
|
||||
mesh := mt.New(t)
|
||||
mesh.Issue(t, mt.MembershipOf("zsh", "anchor", true, map[string][]string{"node-login-shell": {"execute", seatdef.ApprovalsVerb}}))
|
||||
nodeTools := connect(t, "node-tools", "anchor")
|
||||
gate := &gateHolding{}
|
||||
defined := WithSeatDefinitions(func() map[string]seatdef.Definition {
|
||||
return map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell", Verbs: []seatdef.Verb{
|
||||
{Name: "execute", NeedsApproval: true}, {Name: seatdef.ApprovalsVerb}}}}
|
||||
})
|
||||
stop, err := Run(nodeTools, []Served{{"zsh", []string{mt.Fixture("login-shell-execute.py")}}}, nil, func(string, ...any) {}, defined, WithGate(gate))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
asker, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "workstation", User: "workstation.node-tools", Password: "unused"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asker.Logf = func(string, ...any) {}
|
||||
defer asker.Close()
|
||||
|
||||
mt.Until(t, func() error {
|
||||
_, err := asker.Ask("seat:node-login-shell.execute@anchor", map[string]any{"command": "ls"}, "")
|
||||
return err
|
||||
})
|
||||
if len(gate.calls) != 1 || gate.calls[0].Seat != "node-login-shell" || gate.calls[0].Name != "execute" || gate.calls[0].Caller != "workstation.node-tools" {
|
||||
t.Fatalf("the marked verb reached the gate as %+v, want the seat's execute by workstation.node-tools", gate.calls)
|
||||
}
|
||||
var got approval.Approvals
|
||||
mt.Until(t, func() error {
|
||||
answered, err := asker.Ask("seat:node-login-shell.approvals@anchor", map[string]any{}, "")
|
||||
if err == nil {
|
||||
err = json.Unmarshal(answered.Result, &got)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if len(got.Calls) != 1 || got.Calls[0].Caller != "workstation.node-tools" || got.Calls[0].Verb != "node-login-shell.execute" ||
|
||||
string(got.Calls[0].Arguments) != `{"command":"ls"}` {
|
||||
t.Fatalf("approvals answered %+v, want the one call the gate holds, with its caller's arguments", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// Design 47 G A-249: a bundle's seat call is received by the runner in chunks only for a verb whose definition says `chunks`,
|
||||
// of any machine: a verb a node holds is asked of the node that ran the ask (A-254).
|
||||
func TestOnlyAChunkedSeatVerbIsAskedInChunks(t *testing.T) {
|
||||
c := &consumers{definitions: func() map[string]seatdef.Definition {
|
||||
return map[string]seatdef.Definition{"mesh-catalog": {Verbs: []seatdef.Verb{{Name: "input", Chunks: true}, {Name: "entry"}}}}
|
||||
}}
|
||||
for key, want := range map[string]bool{
|
||||
"seat:mesh-catalog.input": true,
|
||||
"seat:mesh-catalog.entry": false,
|
||||
"seat:mesh-catalog.input@ace": true,
|
||||
"seat:mesh-catalog.entry@ace": false,
|
||||
"seat:mesh-unread.input": false,
|
||||
"mesh-delivery-indexer.indexer": false,
|
||||
} {
|
||||
if got := c.chunked(key); got != want {
|
||||
t.Errorf("chunked(%s) = %v, want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
if (&consumers{}).chunked("seat:mesh-catalog.input") {
|
||||
t.Error("a runner that holds no seat definitions asks no chunks")
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/approval"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
|
||||
// gateHolding is the runner's gate as far as the runtime reaches it: it records the calls it is run for and answers
|
||||
@@ -31,49 +24,3 @@ func (g *gateHolding) Approvals(caller string) (approval.Approvals, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// **The tool runner answers a held seat's marked verb by caller and serves the seat's `approvals`** (design 47 G §677; lead
|
||||
// answers A-265, A-271), with zsh's node-login-shell `execute` as the case: the marked verb reaches the gate with the caller
|
||||
// its subject names, and `approvals` is answered from the runner's own record of the calls its gate holds, with the
|
||||
// arguments for the call's own caller only, though no bundle names the verb.
|
||||
func TestTheRunnerServesApprovalsForTheSeatsItsBundlesHold(t *testing.T) {
|
||||
mesh := mt.New(t)
|
||||
mesh.Issue(t, mt.MembershipOf("zsh", "anchor", true, map[string][]string{"node-login-shell": {"execute", seatdef.ApprovalsVerb}}))
|
||||
nodeTools := connect(t, "node-tools", "anchor")
|
||||
gate := &gateHolding{}
|
||||
defined := WithSeatDefinitions(func() map[string]seatdef.Definition {
|
||||
return map[string]seatdef.Definition{"node-login-shell": {Seat: "node-login-shell", Verbs: []seatdef.Verb{
|
||||
{Name: "execute", NeedsApproval: true}, {Name: seatdef.ApprovalsVerb}}}}
|
||||
})
|
||||
stop, err := Run(nodeTools, []Served{{"zsh", []string{mt.Fixture("login-shell-execute.py")}}}, nil, func(string, ...any) {}, defined, WithGate(gate))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer stop()
|
||||
asker, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "shanks", User: "shanks.node-tools", Password: "unused"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asker.Logf = func(string, ...any) {}
|
||||
defer asker.Close()
|
||||
|
||||
mt.Until(t, func() error {
|
||||
_, err := asker.Ask("seat:node-login-shell.execute@anchor", map[string]any{"command": "ls"}, "")
|
||||
return err
|
||||
})
|
||||
if len(gate.calls) != 1 || gate.calls[0].Seat != "node-login-shell" || gate.calls[0].Name != "execute" || gate.calls[0].Caller != "shanks.node-tools" {
|
||||
t.Fatalf("the marked verb reached the gate as %+v, want the seat's execute by shanks.node-tools", gate.calls)
|
||||
}
|
||||
var got approval.Approvals
|
||||
mt.Until(t, func() error {
|
||||
answered, err := asker.Ask("seat:node-login-shell.approvals@anchor", map[string]any{}, "")
|
||||
if err == nil {
|
||||
err = json.Unmarshal(answered.Result, &got)
|
||||
}
|
||||
return err
|
||||
})
|
||||
if len(got.Calls) != 1 || got.Calls[0].Caller != "shanks.node-tools" || got.Calls[0].Verb != "node-login-shell.execute" ||
|
||||
string(got.Calls[0].Arguments) != `{"command":"ls"}` {
|
||||
t.Fatalf("approvals answered %+v, want the one call the gate holds, with its caller's arguments", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
||||
@@ -287,4 +288,70 @@ func TestTheLedgerKeepsItsPromisesOnTheBus(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
// 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.
|
||||
t.Run("AnAckedPublishAnswersItsSequenceAndANewSubscriptionHearsOnlyWhatComesAfter", func(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,26 +36,25 @@ func TestNoBusWordReachesABundle(t *testing.T) {
|
||||
t.Errorf("%s did not reach the bundle", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// design 47 G §618, §519: the runtime's own fingerprint reaches no bundle; the fingerprint a module's composed words hold does.
|
||||
t.Run("TheRuntimesOwnFingerprintReachesNoBundle", func(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)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
@@ -64,66 +64,6 @@ func asked(t *testing.T, b *moduleBus, method string, req any, into any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §518, A-21, A-25: a put session writes each chunk as it is taken, carries the description of its first chunk,
|
||||
// and answers the put's own info for the last; a chunk out of order fails the put naming both sequences and leaves none.
|
||||
func TestAPutSessionSealsOnTheLastChunkAndDropsOnADisorder(t *testing.T) {
|
||||
f := &fakeObjects{put: map[string][]byte{}}
|
||||
b := aSession(t, f)
|
||||
whole := bytes.Repeat([]byte("x"), 2*buscalls.ChunkBytes+5)
|
||||
var taken buscalls.ObjectsChunkTaken
|
||||
for seq, at := uint32(1), 0; ; seq++ {
|
||||
end := min(at+buscalls.ChunkBytes, len(whole))
|
||||
req := buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c1", Seq: seq, Last: end == len(whole), Data: whole[at:end]}
|
||||
if seq == 1 {
|
||||
req.Description = "what it is"
|
||||
}
|
||||
if req.Last {
|
||||
var info buscalls.ObjectInfo
|
||||
asked(t, b, buscalls.ObjectsPutChunk, req, &info)
|
||||
if info.Size != uint64(len(whole)) || info.Description != "what it is" {
|
||||
t.Fatalf("the put answered %+v", info)
|
||||
}
|
||||
break
|
||||
}
|
||||
asked(t, b, buscalls.ObjectsPutChunk, req, &taken)
|
||||
if taken.Seq != seq || taken.Error != "" {
|
||||
t.Fatalf("chunk %d was answered %+v", seq, taken)
|
||||
}
|
||||
at = end
|
||||
}
|
||||
if !bytes.Equal(f.put["a"], whole) {
|
||||
t.Fatalf("the store holds %d bytes, want %d", len(f.put["a"]), len(whole))
|
||||
}
|
||||
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 1, Data: []byte("one")}, &taken)
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 3, Data: []byte("three")}, &taken)
|
||||
if !strings.Contains(taken.Error, "chunk 3") || !strings.Contains(taken.Error, "chunk 2") {
|
||||
t.Fatalf("a disorder was answered %+v", taken)
|
||||
}
|
||||
if _, ok := f.put["b"]; ok || len(f.deleted) != 1 || f.deleted[0] != "b" {
|
||||
t.Fatalf("a dropped put left %v, deleted %v", f.put, f.deleted)
|
||||
}
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 2, Data: []byte("two")}, &taken)
|
||||
if taken.Error == "" {
|
||||
t.Fatal("a chunk of a dropped put was taken")
|
||||
}
|
||||
}
|
||||
|
||||
// §518 "The bound": a put not sealed within the bound is dropped, its object deleted.
|
||||
func TestAPutNotSealedWithinTheBoundIsDropped(t *testing.T) {
|
||||
was := SessionBound
|
||||
SessionBound = time.Millisecond
|
||||
t.Cleanup(func() { SessionBound = was })
|
||||
f := &fakeObjects{put: map[string][]byte{}, ctxEnded: make(chan struct{})}
|
||||
b := aSession(t, f)
|
||||
var taken buscalls.ObjectsChunkTaken
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c", Seq: 1, Data: []byte("x")}, &taken)
|
||||
<-f.ctxEnded
|
||||
if len(f.deleted) != 1 {
|
||||
t.Fatalf("the unsealed object was left: %v", f.deleted)
|
||||
}
|
||||
}
|
||||
|
||||
// §490, §518, §620: a value over one chunk is got as a session (the chunked answer, then each chunk by its call and sequence,
|
||||
// the whole checked against the digest); a sent object over one chunk the same, with `chunked` true.
|
||||
func TestAGetOverOneChunkRunsAsASession(t *testing.T) {
|
||||
@@ -192,4 +132,62 @@ func TestAGetOverOneChunkRunsAsASession(t *testing.T) {
|
||||
if string(small.Data) != "small" {
|
||||
t.Fatalf("a small value was answered %+v", small)
|
||||
}
|
||||
// Design 47 G §518, A-21, A-25: a put session writes each chunk as it is taken, carries the description of its first chunk,
|
||||
// and answers the put's own info for the last; a chunk out of order fails the put naming both sequences and leaves none.
|
||||
t.Run("APutSessionSealsOnTheLastChunkAndDropsOnADisorder", func(t *testing.T) {
|
||||
f := &fakeObjects{put: map[string][]byte{}}
|
||||
b := aSession(t, f)
|
||||
whole := bytes.Repeat([]byte("x"), 2*buscalls.ChunkBytes+5)
|
||||
var taken buscalls.ObjectsChunkTaken
|
||||
for seq, at := uint32(1), 0; ; seq++ {
|
||||
end := min(at+buscalls.ChunkBytes, len(whole))
|
||||
req := buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c1", Seq: seq, Last: end == len(whole), Data: whole[at:end]}
|
||||
if seq == 1 {
|
||||
req.Description = "what it is"
|
||||
}
|
||||
if req.Last {
|
||||
var info buscalls.ObjectInfo
|
||||
asked(t, b, buscalls.ObjectsPutChunk, req, &info)
|
||||
if info.Size != uint64(len(whole)) || info.Description != "what it is" {
|
||||
t.Fatalf("the put answered %+v", info)
|
||||
}
|
||||
break
|
||||
}
|
||||
asked(t, b, buscalls.ObjectsPutChunk, req, &taken)
|
||||
if taken.Seq != seq || taken.Error != "" {
|
||||
t.Fatalf("chunk %d was answered %+v", seq, taken)
|
||||
}
|
||||
at = end
|
||||
}
|
||||
if !bytes.Equal(f.put["a"], whole) {
|
||||
t.Fatalf("the store holds %d bytes, want %d", len(f.put["a"]), len(whole))
|
||||
}
|
||||
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 1, Data: []byte("one")}, &taken)
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 3, Data: []byte("three")}, &taken)
|
||||
if !strings.Contains(taken.Error, "chunk 3") || !strings.Contains(taken.Error, "chunk 2") {
|
||||
t.Fatalf("a disorder was answered %+v", taken)
|
||||
}
|
||||
if _, ok := f.put["b"]; ok || len(f.deleted) != 1 || f.deleted[0] != "b" {
|
||||
t.Fatalf("a dropped put left %v, deleted %v", f.put, f.deleted)
|
||||
}
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 2, Data: []byte("two")}, &taken)
|
||||
if taken.Error == "" {
|
||||
t.Fatal("a chunk of a dropped put was taken")
|
||||
}
|
||||
})
|
||||
// §518 "The bound": a put not sealed within the bound is dropped, its object deleted.
|
||||
t.Run("APutNotSealedWithinTheBoundIsDropped", func(t *testing.T) {
|
||||
was := SessionBound
|
||||
SessionBound = time.Millisecond
|
||||
t.Cleanup(func() { SessionBound = was })
|
||||
f := &fakeObjects{put: map[string][]byte{}, ctxEnded: make(chan struct{})}
|
||||
b := aSession(t, f)
|
||||
var taken buscalls.ObjectsChunkTaken
|
||||
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c", Seq: 1, Data: []byte("x")}, &taken)
|
||||
<-f.ctxEnded
|
||||
if len(f.deleted) != 1 {
|
||||
t.Fatalf("the unsealed object was left: %v", f.deleted)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
//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