The seat is defined whole in mesh-contracts (ADR 0613 §3), so mesh-sdk takes its names from there (ADR 0613 §5):
EngineSeat and EngineEvents are nodeengine's, EngineComposeCandidateVerb is nodeengine.VerbComposeCandidate, and
ComposeCandidateAsk, ComposeCandidateAnswer and ModuleComposed are aliases of nodeengine's ComposeCandidateInput,
ComposeCandidateAnswer and Module. The encode and decode checks stay here over those types until the seat has a client
package (ADR 0625 §4). For importers: a module's fingerprint is Fp, and the answer's Input is a JSON object
(map[string]any), not raw bytes. compose-candidate's input is the boolean the engine and the checker read; the seat
set's test takes that correction, and the whole seat's added verbs as da653ce reads them. EngineSeat leaves
testdata/seat-names.txt.
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
163 lines
7.6 KiB
Go
163 lines
7.6 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// The verb compose-candidate's ask and answer, and a composition that carries its plan or its refusal (ADR 0593 §6, §7):
|
|
// each round-trips, and a reader refuses what it could not take, naming it.
|
|
func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) {
|
|
key, err := CompositionKey("mesh-sdk#7", "0123456789ab")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := EncodeComposeCandidateAsk(ComposeCandidateAsk{Candidate: key, Input: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a, err := DecodeComposeCandidateAsk(raw); err != nil || a.Candidate != key || !a.Input {
|
|
t.Fatalf("ask round trip: %v, %+v", err, a)
|
|
}
|
|
if _, err := DecodeComposeCandidateAsk([]byte(`{"candidate":"` + key + `","other":1}`)); err == nil {
|
|
t.Error("an ask with an unknown key was read")
|
|
}
|
|
if _, err := EncodeComposeCandidateAsk(ComposeCandidateAsk{Candidate: "nope"}); err == nil {
|
|
t.Error("an ask naming no candidate key was encoded")
|
|
}
|
|
|
|
good := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateRefused, Took: 812,
|
|
Own: ModuleComposed{Module: "node", State: ModuleComposedState, Digest: "sha256:00"},
|
|
Modules: []ModuleComposed{
|
|
{Module: "netcheck", Fp: "src2:x", State: ModuleComposedState, Digest: "sha256:01", Changed: true},
|
|
{Module: "route-proxy", State: ModuleRefusedState, Words: "the setting domain has no value"},
|
|
{Module: "zz", State: ModuleHeldState, Words: "held before"},
|
|
}, Input: map[string]any{"machines": []any{"ace"}}}
|
|
raw, err = EncodeComposeCandidateAnswer(good)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeComposeCandidateAnswer(raw)
|
|
if err != nil || back.Verdict != ComposeCandidateRefused || len(back.Modules) != 3 || !reflect.DeepEqual(back.Input, good.Input) {
|
|
t.Fatalf("answer round trip: %v, %+v", err, back)
|
|
}
|
|
for name, mutate := range map[string]func(a *ComposeCandidateAnswer){
|
|
"verdict not the modules'": func(a *ComposeCandidateAnswer) { a.Verdict = ComposeCandidateComposed },
|
|
"unknown state": func(a *ComposeCandidateAnswer) { a.Modules[0].State = "fine" },
|
|
"composed without digest": func(a *ComposeCandidateAnswer) { a.Modules[0].Digest = "" },
|
|
"refused without words": func(a *ComposeCandidateAnswer) { a.Modules[1].Words = "" },
|
|
"unsorted": func(a *ComposeCandidateAnswer) { a.Modules[0], a.Modules[1] = a.Modules[1], a.Modules[0] },
|
|
"own misnamed": func(a *ComposeCandidateAnswer) { a.Own.Module = "ace" },
|
|
"whole refusal w/ modules": func(a *ComposeCandidateAnswer) { a.Refused = "the bound passed" },
|
|
} {
|
|
a := good
|
|
a.Modules = append([]ModuleComposed(nil), good.Modules...)
|
|
mutate(&a)
|
|
if _, err := EncodeComposeCandidateAnswer(a); err == nil {
|
|
t.Errorf("%s: encoded", name)
|
|
}
|
|
}
|
|
whole := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateRefused, Refused: "the bound passed"}
|
|
if _, err := EncodeComposeCandidateAnswer(whole); err != nil {
|
|
t.Errorf("a whole refusal was not encoded: %v", err)
|
|
}
|
|
unready := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateUnready, Refused: "the agent is starting"}
|
|
if raw, err := EncodeComposeCandidateAnswer(unready); err != nil {
|
|
t.Errorf("an unready answer was not encoded: %v", err)
|
|
} else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateUnready {
|
|
t.Errorf("an unready answer read back as %+v, %v", back, err)
|
|
}
|
|
unready.Refused = ""
|
|
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
|
t.Error("an unready answer saying no why was encoded")
|
|
}
|
|
unready.Refused, unready.Modules = "starting", good.Modules
|
|
if _, err := EncodeComposeCandidateAnswer(unready); err == nil {
|
|
t.Error("an unready answer naming modules was encoded")
|
|
}
|
|
// A node the sandbox did not judge: its own verdict, with words and no module (ADR 0593 §9, ADR 0601 §4).
|
|
notJudged := ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: ComposeCandidateNotJudged, Refused: "its node-engine sends no running state"}
|
|
if raw, err := EncodeComposeCandidateAnswer(notJudged); err != nil {
|
|
t.Errorf("a not-judged answer was not encoded: %v", err)
|
|
} else if back, err := DecodeComposeCandidateAnswer(raw); err != nil || back.Verdict != ComposeCandidateNotJudged {
|
|
t.Errorf("a not-judged answer read back as %+v, %v", back, err)
|
|
}
|
|
for name, mutate := range map[string]func(*ComposeCandidateAnswer){
|
|
"no why": func(a *ComposeCandidateAnswer) { a.Refused = "" },
|
|
"modules too": func(a *ComposeCandidateAnswer) { a.Modules = good.Modules },
|
|
} {
|
|
a := notJudged
|
|
mutate(&a)
|
|
if _, err := EncodeComposeCandidateAnswer(a); err == nil {
|
|
t.Errorf("a not-judged answer with %s was encoded", name)
|
|
}
|
|
}
|
|
if _, err := DecodeComposeCandidateAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil {
|
|
t.Error("an answer with an unknown key was read")
|
|
}
|
|
|
|
fp := "src2:" + strings.Repeat("a", 64)
|
|
planned := Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{},
|
|
Plan: &catalogue.Plan{Files: []catalogue.PlanFile{}}}
|
|
raw, err = EncodeComposed(planned)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := DecodeComposed(raw)
|
|
if err != nil || c.Plan == nil {
|
|
t.Fatalf("a composition's plan did not round-trip: %v, %+v", err, c)
|
|
}
|
|
d1, _ := ComposedDigestOf(planned)
|
|
d0, _ := ComposedDigestOf(Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}})
|
|
if d1 == "" || d1 != d0 {
|
|
t.Error("the digest moves with the plan: it is the resources' alone")
|
|
}
|
|
if SameComposed(planned, Composed{Node: "ace", Module: "netcheck", FP: fp, Resources: []json.RawMessage{}}) {
|
|
t.Error("a plan change is not a new composition value")
|
|
}
|
|
signed := strings.Replace(string(raw), `"resources"`, `"signature":{"signer":"x","sig":"y"},"resources"`, 1)
|
|
if c, err := DecodeComposed([]byte(signed)); err != nil || c.Node != "ace" {
|
|
t.Fatalf("a composition the ended controller signed does not decode: %v", err)
|
|
}
|
|
if strings.Contains(string(raw), "signature") {
|
|
t.Error("a composition is written with a signature")
|
|
}
|
|
refused := Composed{Node: "ace", Module: "netcheck", FP: fp, Refused: "the setting domain has no value"}
|
|
if raw, err = EncodeComposed(refused); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c, err := DecodeComposed(raw); err != nil || c.Refused != refused.Refused {
|
|
t.Fatalf("a refused composition did not round-trip: %v", err)
|
|
}
|
|
if _, err := ComposedDigestOf(refused); err == nil {
|
|
t.Error("a refused composition has a digest")
|
|
}
|
|
refused.Data = true
|
|
if _, err := EncodeComposed(refused); err == nil {
|
|
t.Error("a refused composition holding data was encoded")
|
|
}
|
|
if _, err := EncodeComposed(Composed{Node: "ace", Resources: []json.RawMessage{}, Refused: "x"}); err == nil {
|
|
t.Error("the node's own composition carried a refusal")
|
|
}
|
|
}
|
|
|
|
// A kept import form is set only with imports, and in the fingerprint's form (ADR 0593 §4).
|
|
func TestAKeptImportFormNeedsImports(t *testing.T) {
|
|
form := SourcePrefix2 + strings.Repeat("b", 64)
|
|
in := KeptInputs2{Tree: "t", Imports: []string{"git.novox.be/novox/mesh-sdk/go"}, ImportForm: form}
|
|
m := Module{FP2: form, Manifest: strings.Repeat("c", 64), Inputs2: &in, Repository: "novox/x", Commit: "c"}
|
|
if err := m.check(); err != nil && strings.Contains(err.Error(), "import form") {
|
|
t.Fatalf("a good import form refused: %v", err)
|
|
}
|
|
for _, bad := range []KeptInputs2{{Tree: "t", ImportForm: form}, {Tree: "t", Imports: in.Imports, ImportForm: "src2:short"}} {
|
|
m.Inputs2 = &bad
|
|
if err := m.check(); err == nil || !strings.Contains(err.Error(), "import form") {
|
|
t.Errorf("%+v: %v", bad, err)
|
|
}
|
|
}
|
|
}
|