Issue a key-value entry asked in requires as a state, by its local name
mesh/delivery merged: source.main moved to novox/mesh-tools@2ea4f019e594
mesh/delivery-group group fix/1966-requires-state merged: every member is merged
mesh/check checking 8052fd14f4fd on 2ea4f019e594 | novox/mesh-tools#66 in group fix/1966-requires-state

A bundle's state call on a key-value entry its module asks for in requires
(ADR 0595 §1) was refused "keeps and reads no state called", because the
tool runner read the old state list alone (issue 1966). It now reads mesh-sdk's
one rule, membership.StateIssued.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-09 12:39:04 +01:00
parent 9f61975c50
commit 8052fd14f4
2 changed files with 34 additions and 10 deletions
+23 -1
View File
@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/membership"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
@@ -35,16 +37,24 @@ func TestABundleKeepsAndWatchesItsStateThroughTheRuntime(t *testing.T) {
peeker.State = []bus.StateIssued{{Name: "keeper.servers", Bucket: "keeper_servers"}}
mesh.Issue(t, keeper)
mesh.Issue(t, peeker)
// Issue 1966 (F1): a module that asks for its state as a key-value entry of `requires` (ADR 0595 §1) is issued it by
// its binding alone, under its local name, as its own; no other module is.
mesh.Bucket(t, "binder_holds")
binder := mt.MembershipOf("binder", "anchor", false, nil)
binder.Bindings = []membership.BusBinding{{Local: "holds", Storage: catalogue.StorageKeyValue, Template: catalogue.TemplateState, Bucket: "binder_holds"}}
mesh.Issue(t, binder)
nodeTools := connect(t, "node-tools", "anchor")
asker := connect(t, "console", "workstation")
dir := t.TempDir()
keeperLog, peekerLog := filepath.Join(dir, "keeper.log"), filepath.Join(dir, "peeker.log")
keeperLog, peekerLog, binderLog := filepath.Join(dir, "keeper.log"), filepath.Join(dir, "peeker.log"), filepath.Join(dir, "binder.log")
said := &lines{}
stop, err := Run(nodeTools, []Served{
{Module: "keeper", Entrypoints: []string{mt.Fixture("state-keeper.mjs")}},
{Module: "peeker", Entrypoints: []string{mt.Fixture("state-keeper.mjs")}},
{Module: "binder", Entrypoints: []string{mt.Fixture("state-keeper.mjs")}},
}, map[string]map[string]string{
"binder": {"STATE_NAME": "holds", "STATE_LOG": binderLog},
"keeper": {"STATE_NAME": "servers", "STATE_LOG": keeperLog},
"peeker": {"STATE_NAME": "keeper.servers", "STATE_LOG": peekerLog},
}, said.logf)
@@ -91,6 +101,16 @@ func TestABundleKeepsAndWatchesItsStateThroughTheRuntime(t *testing.T) {
}
same(t, got, `["all.one","anchor.two"]`)
// The bound module puts, gets and watches its key-value entry by its local name.
waitFor(t, binderLog, "current")
if _, err := call(t, asker, "binder.put@anchor", map[string]any{"state": "holds", "key": "h1", "value": 1}); err != nil {
t.Fatalf("a put on a bound key-value entry: %v", err)
}
waitFor(t, binderLog, "now put h1 1")
if got, err := call(t, asker, "binder.get@anchor", map[string]any{"state": "holds", "key": "h1"}); err != nil || !strings.Contains(got, `"value":1`) {
t.Fatalf("a get on a bound key-value entry answered %s, %v", got, err)
}
// Refused, with the reason, before anything is sent.
for _, c := range []struct {
key, why string
@@ -100,6 +120,8 @@ func TestABundleKeepsAndWatchesItsStateThroughTheRuntime(t *testing.T) {
map[string]any{"state": "keeper.servers", "key": "x", "value": 1}},
{"keeper.get@anchor", `keeper keeps and reads no state called "other"`,
map[string]any{"state": "other", "key": "x"}},
{"keeper.get@anchor", `keeper keeps and reads no state called "holds"`,
map[string]any{"state": "holds", "key": "h1"}},
{"keeper.put@anchor", `carries a field "accessToken", which names a credential`,
map[string]any{"state": "servers", "key": "x", "value": map[string]any{"headers": map[string]any{"accessToken": "s"}}}},
{"keeper.put@anchor", "is not a key the bus can hold",