The node-engine speaks operator-channel, mesh-vault and node-engine through their contracts and clients; it serves no pull-waits

ADR 0627 §1, §4: mesh-sdk keeps no asks, asker, vault answers or compose-candidate checks, so the node-engine takes them
from mesh-contracts: operator-channel's contract and client (asks, warrants, the asker's client), mesh-vault's sign verb
and answer, and node-engine's own contract and client for compose-candidate's ask, answer, verdicts and bound. The
lead's ruling on pull-waits (no record gives the seat that verb): the verb and its handler go; the row that read the
waits now holds the seat refusing the verb, and failed before the change. The go.mod requires of mesh-contracts' modules
come with the group's pin.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:02:44 +01:00
parent d8207358b0
commit 2b14f166de
14 changed files with 129 additions and 147 deletions
+20 -18
View File
@@ -8,6 +8,8 @@ import (
"testing"
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/agent"
@@ -76,7 +78,7 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = s.Run(ctx) }()
if a := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted}); a.Verdict != delivery.ComposeCandidateUnready || a.Refused == "" {
if a := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: accepted}); a.Verdict != nec.Unready || a.Refused == "" {
t.Errorf("a shadow not ready answered %+v", a)
}
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
@@ -98,46 +100,46 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
time.Sleep(5 * time.Millisecond)
}
a := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted})
if _, err := delivery.EncodeComposeCandidateAnswer(a); err != nil {
a := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: accepted})
if _, err := nec.EncodeAnswer(a); err != nil {
t.Fatalf("the answer does not fit the seat's shape: %v (%+v)", err, a)
}
if a.Verdict != delivery.ComposeCandidateComposed || len(a.Modules) != 1 || a.Modules[0].Module != "lemurs" ||
a.Modules[0].State != delivery.ModuleComposedState || a.Modules[0].Fp != fpNew || !a.Modules[0].Changed ||
a.Own.Module != delivery.HaveNodeWord || a.Took > delivery.ComposeAnswerBound.Milliseconds() {
if a.Verdict != nec.Composed || len(a.Modules) != 1 || a.Modules[0].Module != "lemurs" ||
a.Modules[0].State != nec.ModuleComposed || a.Modules[0].Fp != fpNew || !a.Modules[0].Changed ||
a.Own.Module != delivery.HaveNodeWord || a.Took > nec.AnswerBound.Milliseconds() {
t.Errorf("an accepted candidate answered %+v", a)
}
// Issue 774: the input it sends the sandbox says how it composed the node (the builds), so the sandbox composes it
// through the same path.
sent := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: accepted, Input: true})
sent := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: accepted, Input: true})
var in compose.Input
raw, _ := json.Marshal(sent.Input)
if err := json.Unmarshal(raw, &in); err != nil || in.ComposedHow == nil || in.ComposedHow.Node != "n1" ||
in.ComposedHow.Builds["lemurs"] != fpNew {
t.Errorf("the input sent for the sandbox does not say how the node composes: %+v, %v", in.ComposedHow, err)
}
r := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: refusedKey})
if r.Verdict != delivery.ComposeCandidateRefused || len(r.Modules) != 1 || r.Modules[0].State != delivery.ModuleRefusedState ||
r := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: refusedKey})
if r.Verdict != nec.Refused || len(r.Modules) != 1 || r.Modules[0].State != nec.ModuleRefused ||
!strings.Contains(r.Modules[0].Words, "a field the judge refuses") {
t.Errorf("a candidate whose manifest the indexer refused answered %+v", r)
}
if _, err := delivery.EncodeComposeCandidateAnswer(r); err != nil {
if _, err := nec.EncodeAnswer(r); err != nil {
t.Errorf("the refused answer does not fit the seat's shape: %v", err)
}
missing, _ := delivery.CompositionKey("novox/x#8", "0123456789ab")
if m := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: missing}); !strings.Contains(m.Refused, "holds no candidate") {
if m := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: missing}); !strings.Contains(m.Refused, "holds no candidate") {
t.Errorf("a candidate the indexer does not hold answered %+v", m)
}
// The group's candidate moved to other heads since the ask: the node composes not the one asked, and answers
// unready (no answer), naming the heads it holds.
moved, _ := delivery.CompositionKey("fix/940-no-retire-on-partial-read", "0123456789ab")
if m := s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: moved}); m.Verdict != delivery.ComposeCandidateUnready ||
if m := s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: moved}); m.Verdict != nec.Unready ||
!strings.Contains(m.Refused, "heads") {
t.Errorf("a candidate at other heads answered %+v", m)
}
ended, end := context.WithCancel(ctx)
end()
if e := s.ComposeCandidate(ended, delivery.ComposeCandidateAsk{Candidate: accepted}); e.Verdict != delivery.ComposeCandidateUnready {
if e := s.ComposeCandidate(ended, nodeengine.ComposeCandidateInput{Candidate: accepted}); e.Verdict != nec.Unready {
t.Errorf("an ended bound answered %+v", e)
}
if len(state.written()) != 0 || applied.Load() != 0 || handed.Load() != 0 {
@@ -215,11 +217,11 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
words string // in the answer's words
said string // in the line said of the last answer
}{
{name: "one ask reads each manifest once", asks: 1, verdict: delivery.ComposeCandidateComposed, reads: 1,
{name: "one ask reads each manifest once", asks: 1, verdict: nec.Composed, reads: 1,
said: "compose-candidate fix/1126."},
{name: "a later ask reads none again", asks: 2, verdict: delivery.ComposeCandidateComposed, reads: 1,
{name: "a later ask reads none again", asks: 2, verdict: nec.Composed, reads: 1,
said: "read(s) waited on"},
{name: "a read that does not answer is named at the bound", stuck: true, asks: 1, verdict: delivery.ComposeCandidateUnready,
{name: "a read that does not answer is named at the bound", stuck: true, asks: 1, verdict: nec.Unready,
words: "waits on", said: "answered unready"},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -241,9 +243,9 @@ func TestTheNodeComposesACandidateOnAsk(t *testing.T) {
return get(rctx, bucket, k)
}
}
var a delivery.ComposeCandidateAnswer
var a nodeengine.ComposeCandidateAnswer
for range tc.asks {
a = s.ComposeCandidate(ctx, delivery.ComposeCandidateAsk{Candidate: key})
a = s.ComposeCandidate(ctx, nodeengine.ComposeCandidateInput{Candidate: key})
}
if a.Verdict != tc.verdict || !strings.Contains(a.Refused, tc.words) {
t.Errorf("answered %s %q; want %s with %q", a.Verdict, a.Refused, tc.verdict, tc.words)