R-11: mesh/publish with ack answers its sequence or {error}, mesh/subscribe with deliver new makes an ordered consumer; finding-calls is stated
The subscription's key resolves through mesh-sdk's buscompose.EventSubjects (lead answer A-153; the sdk commit EE-S1 is cherry-picked into this change's sdk placement), a wildcard is refused. Builds: design 47 G §559, §488, §576, §622 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -142,7 +142,7 @@ 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] != "bus-state-calls,ledger-calls,ledger-upkeep-calls,put-sessions,seat-settle-delay,sent-objects-calls" {
|
||||
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,10 @@ var callFamilies = map[string][]string{
|
||||
}
|
||||
|
||||
// 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}
|
||||
out := []string{stdio.CapSeatSettleDelay, stdio.CapFindingCalls}
|
||||
for c := range callFamilies {
|
||||
out = append(out, c)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ 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 != "bus-state-calls,ledger-calls,ledger-upkeep-calls,put-sessions,seat-settle-delay,sent-objects-calls" {
|
||||
if got := strings.Join(CallCapabilities(), ","); got != "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)
|
||||
}
|
||||
for capability, methods := range callFamilies {
|
||||
|
||||
@@ -876,6 +876,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 {
|
||||
|
||||
@@ -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