The node-engine serves manifest-reads and says manifest-reads-changed once per link (ADR 0618 §3, §4, issue 774)

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
2026-11-04 12:38:30 +01:00
parent c93feeb30f
commit 271de0108d
6 changed files with 95 additions and 1 deletions
+44
View File
@@ -12,6 +12,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/micro"
)
@@ -191,6 +192,20 @@ type handingLink struct {
given chan HandOverFunc
}
// readsLink is a quiet link that serves the node seat and keeps the manifest-reads-changed events said on it.
type readsLink struct {
*quietLink
said chan delivery.ManifestReadsChanged
}
func (l readsLink) ServeEngineSeat(EngineFunc) {}
func (l readsLink) SayEngine(event string, body any) error {
if e, err := delivery.DecodeManifestReadsChanged(body.(json.RawMessage)); event == "manifest-reads-changed" && err == nil {
l.said <- e
}
return nil
}
func (l handingLink) HearHandOvers(h HandOverFunc) { l.given <- h }
// **The link is given the queue's hand-over** when it is served: what the controller asks reaches the engine's
@@ -213,6 +228,30 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
}
stop()
<-served
// ADR 0618 §4: an engine that holds the seat says manifest-reads-changed once for each link it serves, "started" on
// the first and "returned" on every one after it, and never once per pass.
said := make(chan delivery.ManifestReadsChanged, 4)
for _, why := range []string{delivery.ReadsStarted, delivery.ReadsReturned} {
el := readsLink{quietLink: newQuietLink(), said: said}
eq := q
eq.Acts = &HandActs{Node: "n1", State: &MemState{}}
ectx, estop := context.WithCancel(context.Background())
edone := make(chan error, 1)
go func() { edone <- serve(ectx, el, m, eq, nil, time.Second) }()
select {
case got := <-said:
if got.Why != why || got.Node != "n1" {
t.Errorf("the link said %+v, want %s", got, why)
}
case <-time.After(5 * time.Second):
t.Fatalf("the link %s said nothing", why)
}
estop()
<-edone
}
if len(said) != 0 {
t.Errorf("more than once per link: %d more said", len(said))
}
// 774 (ADR 0563 §1): a call of the node seat reaches its verb with its caller as the subject proves it, unproven on
// the subject that names none; a verb the seat does not serve is refused, and the answer is a tool's envelope.
t.Run("a call of the node seat reaches its verb with its proven caller", func(t *testing.T) {
@@ -227,6 +266,10 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
PullWaits: func() (any, error) {
return map[string]any{"waits": []map[string]any{{"module": "web", "holder": "distribution", "last-retry": "distribution settled"}}}, nil
},
// ADR 0618 §3: what this engine states it reads, and when it last stated it.
ManifestReads: func() ([]string, time.Time) {
return []string{"form:a", "seat:b"}, time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
},
// ADR 0565 §6, §15: the node's key of the engine's state packages.
Packages: func(write bool) (any, error) {
key := []byte(`{"node":"n1","packages":[{"package":"curl","modules":["backup","web"],"absent":false}]}`)
@@ -255,6 +298,7 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
{"packages", EngineToolSubject("packages", "n1"), `"source":"mesh-host.packages","key":{"node":"n1"`},
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":true}`, `"key":null,"wrote":true`},
{"packages", EngineToolSubject("packages", "n1") + " " + `{"write":"true"}`, `"note":"the engine has written no key for this node yet`},
{"manifest-reads", EngineToolSubject("manifest-reads", "n1"), `"v":1,"node":"n1","manifest-reads":["form:a","seat:b"],"stated-at":"2026-11-04T10:00:00Z"`},
{"unserved", "mesh.seat.node-engine.call.unserved.n1.x", `"error":"the node seat node-engine serves no verb \"unserved\""`},
} {
subject, body, ok := strings.Cut(row.subject, " ")