The node-engine seat's manifest-reads answer and event come from mesh-contracts' nodeengine contract, judged by their reader (ADR 0618 §3, §4, issue 774)
The contract carries no checks, so sdk/enginereads judges the layout's version, the node, each capability, the reason and the times. The local replaces of mesh-sdk are gone: the lead pins mesh-sdk and mesh-contracts at the join. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
@@ -31,6 +31,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"mesh-delivery-assigner/internal/judge"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
@@ -513,7 +514,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
||||
defer nc.Close()
|
||||
if _, err := nc.Subscribe("mesh.seat.node-engine.tool.manifest-reads.ace", func(m *nats.Msg) {
|
||||
statedMu.Lock()
|
||||
answer, _ := delivery.EncodeManifestReadsAnswer(delivery.ManifestReadsAnswer{Node: "ace", ManifestReads: stated, StatedAt: time.Now()})
|
||||
answer, _ := json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: "ace", ManifestReads: stated, StatedAt: time.Now().Format(time.RFC3339Nano)})
|
||||
statedMu.Unlock()
|
||||
reply, _ := json.Marshal(map[string]any{"result": json.RawMessage(answer), "node": "ace"})
|
||||
_ = m.Respond(reply)
|
||||
@@ -521,12 +522,12 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
say := func(why string) {
|
||||
body, _ := delivery.EncodeManifestReadsChanged(delivery.ManifestReadsChanged{Node: "ace", ManifestReads: stated, At: time.Now(), Why: why})
|
||||
body, _ := json.Marshal(nodeengine.ManifestReadsChangedEvent{V: 1, Node: "ace", ManifestReads: stated, At: time.Now().Format(time.RFC3339Nano), Why: why})
|
||||
if _, err := b.js.Publish(b.ctx, enginereads.SubjectPrefix+"ace", body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
say(delivery.ReadsStarted)
|
||||
say(enginereads.ReadsStarted)
|
||||
formsOld, formsNew := fpOf('5'), fpOf('6')
|
||||
formsDigest := b.manifest(t, "forms", `{"module":"forms","version":"1"}`)
|
||||
formsAt := func(fp string) {
|
||||
@@ -578,7 +579,7 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
||||
statedMu.Lock()
|
||||
stated = append(slices.Clone(stated), "form:new")
|
||||
statedMu.Unlock()
|
||||
say(delivery.ReadsStarted)
|
||||
say(enginereads.ReadsStarted)
|
||||
b.wantNow(t, "ace", "forms", "the want moved once ace's engine said it reads the form", func(w delivery.Want) bool { return w.FP == formsNew })
|
||||
// assigner_engine_reads asks ace's engine again through the state: the assigner answers there with what it states now.
|
||||
again, err := enginereads.AskAgainOf(bedState{b, engineReadsBucket}, "ace", "n1.ada", "a person asks", time.Now(), 15*time.Second)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
@@ -177,7 +178,7 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) {
|
||||
if node == "g14" || key != enginereads.Invoke {
|
||||
return nil, &busretry.Answered{Words: "the seat node-engine serves no verb manifest-reads"}
|
||||
}
|
||||
return delivery.EncodeManifestReadsAnswer(delivery.ManifestReadsAnswer{Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now()})
|
||||
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
|
||||
}
|
||||
p := &passer{w: w, reads: &enginereads.Keeper{Call: call, Fallback: w.recordReads, Sync: true}}
|
||||
on, _ := delivery.EncodePresence(delivery.Presence{Online: false})
|
||||
@@ -197,6 +198,43 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) {
|
||||
if asked["ace"] != 2 {
|
||||
t.Errorf("ace, back online, was asked %d times, want 2", asked["ace"])
|
||||
}
|
||||
// The contract carries no checks, so its reader judges what the engine says (mesh-contracts seats/nodeengine): an
|
||||
// answer or an event that does not hold is no statement, and an event's node is its subject's last token.
|
||||
now := time.Now().Format(time.RFC3339Nano)
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
raw string
|
||||
stays bool
|
||||
}{
|
||||
{"a whole answer", `{"v":1,"node":"ace","manifest-reads":["form:new"],"stated-at":"` + now + `"}`, true},
|
||||
{"an engine stating nothing", `{"v":1,"node":"ace","stated-at":"` + now + `"}`, true},
|
||||
{"another layout", `{"v":2,"node":"ace","stated-at":"` + now + `"}`, false},
|
||||
{"no node", `{"v":1,"node":"","stated-at":"` + now + `"}`, false},
|
||||
{"a node that is no token", `{"v":1,"node":"a.ce","stated-at":"` + now + `"}`, false},
|
||||
{"another node's answer", `{"v":1,"node":"g14","stated-at":"` + now + `"}`, false},
|
||||
{"an empty capability", `{"v":1,"node":"ace","manifest-reads":[""],"stated-at":"` + now + `"}`, false},
|
||||
{"a time that is no time", `{"v":1,"node":"ace","stated-at":"yesterday"}`, false},
|
||||
} {
|
||||
s := enginereads.Ask(func(string, string, any) (json.RawMessage, error) { return json.RawMessage(row.raw), nil }, "ace", time.Now())
|
||||
if (s.From == enginereads.FromVerb) != row.stays || !s.Answered {
|
||||
t.Errorf("answer %s: %+v", row.name, s)
|
||||
}
|
||||
}
|
||||
for _, row := range []struct {
|
||||
name, subject, raw string
|
||||
ours, fails bool
|
||||
}{
|
||||
{"the event", enginereads.SubjectPrefix + "ace", `{"v":1,"node":"ace","at":"` + now + `","why":"returned"}`, true, false},
|
||||
{"another reason", enginereads.SubjectPrefix + "ace", `{"v":1,"node":"ace","at":"` + now + `","why":"bored"}`, true, true},
|
||||
{"another node in the body", enginereads.SubjectPrefix + "ace", `{"v":1,"node":"g14","at":"` + now + `","why":"started"}`, true, true},
|
||||
{"a time that is no time", enginereads.SubjectPrefix + "ace", `{"v":1,"node":"ace","at":"x","why":"started"}`, true, true},
|
||||
{"another event", "mesh.seat.node-engine.event.done.ace", `{}`, false, false},
|
||||
} {
|
||||
_, ours, err := enginereads.NodeOf(row.subject, []byte(row.raw))
|
||||
if ours != row.ours || (err != nil) != row.fails {
|
||||
t.Errorf("event %s: ours %v, %v", row.name, ours, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,4 +25,3 @@ require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../0618-sdk/go
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -87,7 +88,7 @@ func theCountedNodesEngineReads(t *testing.T) {
|
||||
if away[node] {
|
||||
return nil, nats.ErrNoResponders
|
||||
}
|
||||
return delivery.EncodeManifestReadsAnswer(delivery.ManifestReadsAnswer{Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now()})
|
||||
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
|
||||
}, gate, t.Logf)
|
||||
r.keeper.Sync = true
|
||||
kept := map[string][]byte{}
|
||||
|
||||
@@ -26,7 +26,6 @@ require (
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ./buildrun
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../0618-sdk/go
|
||||
|
||||
require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
|
||||
@@ -28,4 +28,3 @@ require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../0618-sdk/go
|
||||
|
||||
@@ -4886,7 +4886,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
|
||||
t.Errorf("acts takes no id: %v", engine.Serves[5].Input)
|
||||
}
|
||||
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition,compose,compose-candidate,manifest-reads" ||
|
||||
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition,compose,compose-candidate,pull-waits,manifest-reads" ||
|
||||
strings.Join(engine.Emits, ",") != "asked,done,refused,manifest-reads-changed" {
|
||||
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
|
||||
|
||||
"mesh-delivery-view/internal/view"
|
||||
@@ -594,7 +595,7 @@ func manifestReadsAsksEveryNode(t *testing.T) {
|
||||
<-hang // never answers within the bound
|
||||
return nil, nil
|
||||
}
|
||||
return delivery.EncodeManifestReadsAnswer(delivery.ManifestReadsAnswer{Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now()})
|
||||
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"form:new"}, StatedAt: time.Now().Format(time.RFC3339Nano)})
|
||||
}
|
||||
record := func(node string, reads ...string) string {
|
||||
raw, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, ManifestReads: reads}})
|
||||
|
||||
@@ -25,4 +25,3 @@ require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../0618-sdk/go
|
||||
|
||||
@@ -21,4 +21,3 @@ require (
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../../0618-sdk/go
|
||||
|
||||
@@ -18,19 +18,99 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/busretry"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
)
|
||||
|
||||
// The verb and the event as a manifest names them, and the subject the event is said on with the engine's machine last.
|
||||
// The verb and the event as the seat's contract names them (mesh-contracts seats/nodeengine), as a manifest names them,
|
||||
// and the subject the event is said on with the engine's machine last.
|
||||
const (
|
||||
Verb = "manifest-reads"
|
||||
Event = "manifest-reads-changed"
|
||||
Invoke = "seat:" + catalogue.EngineSeat + "." + Verb
|
||||
Consumes = catalogue.EngineSeat + "." + Event
|
||||
SubjectPrefix = "mesh.seat." + catalogue.EngineSeat + ".event." + Event + "."
|
||||
Verb = nodeengine.VerbManifestReads
|
||||
Event = nodeengine.EventManifestReadsChanged
|
||||
Invoke = "seat:" + nodeengine.Seat + "." + Verb
|
||||
Consumes = nodeengine.Seat + "." + Event
|
||||
)
|
||||
|
||||
// SubjectPrefix is the event's subject up to the machine, its last token.
|
||||
var SubjectPrefix = strings.TrimSuffix(nodeengine.SubjectManifestReadsChanged("*"), "*")
|
||||
|
||||
// The reasons an engine says its reads, in the event's why (ADR 0618 §4).
|
||||
const (
|
||||
// ReadsStarted: the engine started, or what it states changed.
|
||||
ReadsStarted = "started"
|
||||
// ReadsReturned: the engine's node returned after being away.
|
||||
ReadsReturned = "returned"
|
||||
)
|
||||
|
||||
// ContractVersion is the layout version of the verb's answer and the event's body this reader reads.
|
||||
const ContractVersion = 1
|
||||
|
||||
// checkNode refuses a node that is no subject token.
|
||||
func checkNode(node string) error {
|
||||
if node == "" || strings.ContainsAny(node, ".*> \t\n") {
|
||||
return fmt.Errorf("%q is no node", node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkReads refuses an empty reader capability.
|
||||
func checkReads(node string, reads []string) error {
|
||||
for _, c := range reads {
|
||||
if strings.TrimSpace(c) == "" {
|
||||
return fmt.Errorf("the reads of %s name an empty reader capability", node)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeAnswer reads the verb's answer as the contract lays it out, and judges it: the layout's version, the node a
|
||||
// name, no empty capability, and its stated-at an RFC 3339 time (the contract carries no checks; its reader does).
|
||||
func DecodeAnswer(raw []byte) (nodeengine.ManifestReadsAnswer, time.Time, error) {
|
||||
var a nodeengine.ManifestReadsAnswer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return a, time.Time{}, err
|
||||
}
|
||||
if a.V != ContractVersion {
|
||||
return a, time.Time{}, fmt.Errorf("the answer's layout is version %d, not %d", a.V, ContractVersion)
|
||||
}
|
||||
if err := checkNode(a.Node); err != nil {
|
||||
return a, time.Time{}, err
|
||||
}
|
||||
if err := checkReads(a.Node, a.ManifestReads); err != nil {
|
||||
return a, time.Time{}, err
|
||||
}
|
||||
at, err := time.Parse(time.RFC3339Nano, a.StatedAt)
|
||||
if err != nil {
|
||||
return a, time.Time{}, fmt.Errorf("stated-at %q is no RFC 3339 time", a.StatedAt)
|
||||
}
|
||||
return a, at, nil
|
||||
}
|
||||
|
||||
// DecodeChanged reads the event's body as the contract lays it out, and judges it as DecodeAnswer does, its why one of
|
||||
// ReadsStarted and ReadsReturned.
|
||||
func DecodeChanged(raw []byte) (nodeengine.ManifestReadsChangedEvent, error) {
|
||||
var e nodeengine.ManifestReadsChangedEvent
|
||||
if err := json.Unmarshal(raw, &e); err != nil {
|
||||
return e, err
|
||||
}
|
||||
if e.V != ContractVersion {
|
||||
return e, fmt.Errorf("the event's layout is version %d, not %d", e.V, ContractVersion)
|
||||
}
|
||||
if err := checkNode(e.Node); err != nil {
|
||||
return e, err
|
||||
}
|
||||
if e.Why != ReadsStarted && e.Why != ReadsReturned {
|
||||
return e, fmt.Errorf("the reads of %s changed for %q, not %s or %s", e.Node, e.Why, ReadsStarted, ReadsReturned)
|
||||
}
|
||||
if err := checkReads(e.Node, e.ManifestReads); err != nil {
|
||||
return e, err
|
||||
}
|
||||
if _, err := time.Parse(time.RFC3339Nano, e.At); err != nil {
|
||||
return e, fmt.Errorf("at %q is no RFC 3339 time", e.At)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Where a node's statement came from.
|
||||
const (
|
||||
// FromVerb: the node's engine answered the verb.
|
||||
@@ -101,12 +181,15 @@ func Ask(call Caller, node string, now time.Time) Statement {
|
||||
var answered *busretry.Answered
|
||||
switch {
|
||||
case err == nil:
|
||||
a, derr := delivery.DecodeManifestReadsAnswer(raw)
|
||||
a, at, derr := DecodeAnswer(raw)
|
||||
if derr == nil && a.Node != node {
|
||||
derr = fmt.Errorf("it names the node %s", a.Node)
|
||||
}
|
||||
if derr != nil {
|
||||
s.Answered, s.Words = true, "the engine answered what is no statement: "+derr.Error()
|
||||
return s
|
||||
}
|
||||
s.ManifestReads, s.StatedAt, s.From, s.Answered = slices.Clone(a.ManifestReads), a.StatedAt.UTC(), FromVerb, true
|
||||
s.ManifestReads, s.StatedAt, s.From, s.Answered = slices.Clone(a.ManifestReads), at.UTC(), FromVerb, true
|
||||
case errors.As(err, &answered):
|
||||
s.Answered, s.Words = true, "the engine does not serve "+Verb+": "+answered.Words
|
||||
default:
|
||||
@@ -122,7 +205,7 @@ func NodeOf(subject string, data []byte) (string, bool, error) {
|
||||
if !ok || node == "" || strings.Contains(node, ".") {
|
||||
return "", false, nil
|
||||
}
|
||||
e, err := delivery.DecodeManifestReadsChanged(data)
|
||||
e, err := DecodeChanged(data)
|
||||
if err != nil {
|
||||
return node, true, err
|
||||
}
|
||||
|
||||
@@ -13,4 +13,3 @@ require (
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-sdk/go => ../../0618-sdk/go
|
||||
|
||||
Reference in New Issue
Block a user