J1: node-engine's compose-candidate checks leave mesh-sdk for its client; pull-waits leaves the seat-set row (local only)
This commit is contained in:
+10
-11
@@ -1,16 +1,14 @@
|
||||
package catalogue
|
||||
|
||||
import "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
|
||||
|
||||
// EngineSeat is the node seat every node's engine holds.
|
||||
//
|
||||
// Deprecated: the seat is defined whole in mesh-contracts (nodeengine.Seat, its verbs, events, subjects and message
|
||||
// types); kept for its importers until their next change takes it from there (testdata/seat-names.txt).
|
||||
const EngineSeat = nodeengine.Seat
|
||||
// EngineSeat is the node seat every node's engine holds, from the seat set. The seat is defined whole in mesh-contracts
|
||||
// (its contract, go/seats/nodeengine, and its client, go/clients/nodeengineclient): its verbs' and events' names and
|
||||
// message types are taken from there (ADR 0627 §4).
|
||||
const EngineSeat = contractseats.NodeEngine
|
||||
|
||||
// EngineVerbs are the node-engine seat's verbs, in the order ADR 0563 §1 names them: its definition's, read from the
|
||||
// generated seat list (design 26), each with its description and its input's schema as discovery shows them. Their
|
||||
// names are nodeengine.Verbs().
|
||||
// generated seat list (design 26), each with its description and its input's schema as discovery shows them.
|
||||
func EngineVerbs() []Verb {
|
||||
s, _ := seatIn(defaultSeats, EngineSeat)
|
||||
return append([]Verb(nil), s.Serves...)
|
||||
@@ -18,9 +16,10 @@ func EngineVerbs() []Verb {
|
||||
|
||||
// EngineEvents are what the seat says: of an act, each naming the act, the path or access, the caller as the runtime
|
||||
// proved it and the ask (ADR 0563 §1), and that what its engine reads of a manifest changed (ADR 0618 §4).
|
||||
//
|
||||
// Deprecated: the seat's contract names them, nodeengine.Events(); kept for its importers.
|
||||
var EngineEvents = nodeengine.Events()
|
||||
var EngineEvents = func() []string {
|
||||
s, _ := seatIn(defaultSeats, EngineSeat)
|
||||
return append([]string(nil), s.Emits...)
|
||||
}()
|
||||
|
||||
// seatIn is the seat of that name in a list.
|
||||
func seatIn(list []Seat, name string) (Seat, bool) {
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestUseSeatsReplacesTheSetButNeverEmptiesIt(t *testing.T) {
|
||||
whole := []string{"npm-package-registry", "mesh-broker", "mesh-config", "mesh-vault", "mesh-delivery-checker", "node-engine"}
|
||||
dropped := map[string]bool{"mesh-vault.accept": true, "mesh-vault.move-asks": true}
|
||||
// Verbs a seat at the seat list's grain gained in the milestone, each optional until its holders serve it.
|
||||
addedVerbs := map[string]bool{"node-engine.pull-waits": true}
|
||||
addedVerbs := map[string]bool{}
|
||||
// A verb whose input the whole definition corrected: compose-candidate's input is the boolean the engine and the
|
||||
// checker read, where the compiled list said a word (engine_compose_candidate_test.go reads it).
|
||||
retyped := map[string]bool{"node-engine.compose-candidate": true}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
)
|
||||
|
||||
// A composition that does not exist yet is asked of the node-engine (novox/hq ADR 0593 §7): the seat node-engine's verb
|
||||
// compose-candidate, served by each node's engine alone, composes a check's candidate in shadow with the node's own
|
||||
// sealed values and the indexer's manifest verdicts, applies and writes nothing, and answers within the asker's bound.
|
||||
// The checker asks every placed node in parallel and makes the check's composition verdict from the answers; a node
|
||||
// that does not answer within the bound is named "not composed on that node: no answer" and the check passes with that
|
||||
// warning (§9). This is the contract only: the composing is the node-engine's own.
|
||||
|
||||
const (
|
||||
// EngineComposeCandidateVerb is the verb's name on the seat node-engine (catalogue.EngineSeat).
|
||||
//
|
||||
// Deprecated: the seat's contract names it, nodeengine.VerbComposeCandidate; kept for its importers.
|
||||
EngineComposeCandidateVerb = nodeengine.VerbComposeCandidate
|
||||
// ComposeAnswerBound is the default of the checker's setting compose-answer-bound: how long it waits for a node's
|
||||
// answer (ADR 0593 §7, §9).
|
||||
ComposeAnswerBound = 60 * time.Second
|
||||
// ComposeNoAnswer is the words a check's verdict names a node with that did not answer within the bound (ADR 0593 §9).
|
||||
ComposeNoAnswer = "not composed on that node: no answer"
|
||||
|
||||
// ComposeCandidateComposed and ComposeCandidateRefused are an answer's verdict: no module and not the node's own composition is
|
||||
// refused by the candidate, or at least one is (or the node could not compose with it at all, Refused).
|
||||
ComposeCandidateComposed = "composed"
|
||||
ComposeCandidateRefused = "refused"
|
||||
// ComposeCandidateUnready is the verdict of a node that cannot compose yet (its shadow not ready, its agent
|
||||
// starting): no module is named, Refused says why in words, and the checker takes it as a node that did not answer
|
||||
// (ComposeNoAnswer, ADR 0593 §9), deciding by this verdict and never by the words.
|
||||
ComposeCandidateUnready = "unready"
|
||||
// ComposeCandidateNotJudged is the verdict for a node the checker's sandbox composed and could not judge (an older
|
||||
// node-engine's input that does not say how it composes, novox/hq ADR 0593 §9): no module is named, Refused says why in
|
||||
// words, and the checker names the node with them and passes the check with that warning, never silent and never a
|
||||
// failure of the change. The checker decides by this verdict, never by the words (ADR 0601 §4's kind: a contract is a
|
||||
// field, not a phrase).
|
||||
ComposeCandidateNotJudged = "not-judged"
|
||||
|
||||
// The states of one module's composition with the candidate: composed (Digest set), refused by the candidate
|
||||
// (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict.
|
||||
ModuleComposedState = "composed"
|
||||
ModuleRefusedState = "refused"
|
||||
ModuleHeldState = "held"
|
||||
)
|
||||
|
||||
// The verb's ask and answer are the seat's contract, generated in mesh-contracts (seats/node-engine): mesh-sdk keeps
|
||||
// these names for their importers and the checks below over them, until the seat has a client package (ADR 0625 §4).
|
||||
type (
|
||||
// ComposeCandidateAsk is the verb's input: Candidate is the key of the indexer's candidate (CompositionKey,
|
||||
// `<escaped group>.<heads hash>`); the engine reads that candidate itself and lays its modules over its placement.
|
||||
// Input asks the engine to answer too the composition input it composed the candidate from (its own node's scope, the
|
||||
// catalogue left out as composer.Request.Input says): the checker puts it into the request a build module's composer
|
||||
// runs over, for a group that changes the node-engine's composition code (ADR 0593 §7).
|
||||
//
|
||||
// Deprecated: the seat's contract is nodeengine.ComposeCandidateInput; kept for its importers.
|
||||
ComposeCandidateAsk = nodeengine.ComposeCandidateInput
|
||||
// ComposeCandidateAnswer is the verb's answer: the node, the candidate, the verdict, every module placed on the node
|
||||
// sorted by name, the node's own composition (Own, Module "node"), why the node could not compose with the candidate
|
||||
// at all (Refused: Modules is then empty and Verdict refused, or unready when it cannot compose yet), how long the
|
||||
// engine took in milliseconds, and, when asked (ComposeCandidateAsk.Input), the composition input it composed from.
|
||||
//
|
||||
// Deprecated: the seat's contract is nodeengine.ComposeCandidateAnswer; kept for its importers.
|
||||
ComposeCandidateAnswer = nodeengine.ComposeCandidateAnswer
|
||||
// ModuleComposed is one module's composition with the candidate: the fingerprint it was composed at (the
|
||||
// candidate's FP2 for a module the candidate touches, else its newest done build as composed-<node> holds it), its
|
||||
// state, the composition's digest when composed (ComposedDigestOf), whether the candidate changes it from the one
|
||||
// composed without it (the checker's "what the change reaches on this node"), and why, for a refused or held one.
|
||||
//
|
||||
// Deprecated: the seat's contract is nodeengine.Module; kept for its importers.
|
||||
ModuleComposed = nodeengine.Module
|
||||
)
|
||||
|
||||
func checkComposeAsk(a ComposeCandidateAsk) error {
|
||||
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
|
||||
return fmt.Errorf("delivery: a compose-candidate ask names no candidate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkComposedModule(m ModuleComposed, node string) error {
|
||||
if m.Module == "" {
|
||||
return fmt.Errorf("delivery: a module's composition on %s names no module", node)
|
||||
}
|
||||
switch m.State {
|
||||
case ModuleComposedState:
|
||||
if m.Digest == "" {
|
||||
return fmt.Errorf("delivery: %s is composed on %s and names no digest", m.Module, node)
|
||||
}
|
||||
case ModuleRefusedState, ModuleHeldState:
|
||||
if m.Words == "" {
|
||||
return fmt.Errorf("delivery: %s is %s on %s and says no why", m.Module, m.State, node)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("delivery: %s on %s is %q, neither %s, %s nor %s", m.Module, node, m.State,
|
||||
ModuleComposedState, ModuleRefusedState, ModuleHeldState)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkComposeAnswer(a ComposeCandidateAnswer) error {
|
||||
if err := checkName("node", a.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := ParseCompositionKey(a.Candidate); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Took < 0 {
|
||||
return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took)
|
||||
}
|
||||
if (a.Verdict == ComposeCandidateUnready || a.Verdict == ComposeCandidateNotJudged) && a.Refused == "" {
|
||||
return fmt.Errorf("delivery: %s's answer is %s and says no why", a.Node, a.Verdict)
|
||||
}
|
||||
if a.Refused != "" {
|
||||
if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready && a.Verdict != ComposeCandidateNotJudged {
|
||||
return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s, %s nor %s",
|
||||
a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready, ComposeCandidateNotJudged)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
refused := false
|
||||
for i, m := range append([]ModuleComposed{a.Own}, a.Modules...) {
|
||||
if err := checkComposedModule(m, a.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if i > 1 && a.Modules[i-2].Module >= m.Module {
|
||||
return fmt.Errorf("delivery: the modules of %s's answer are not sorted and each once (%s after %s)", a.Node, m.Module, a.Modules[i-2].Module)
|
||||
}
|
||||
refused = refused || m.State == ModuleRefusedState
|
||||
}
|
||||
switch {
|
||||
case a.Own.Module != nodeWord:
|
||||
return fmt.Errorf("delivery: the node's own composition in %s's answer is named %q, not %q", a.Node, a.Own.Module, nodeWord)
|
||||
case refused && a.Verdict != ComposeCandidateRefused:
|
||||
return fmt.Errorf("delivery: %s's answer refuses a module and its verdict is %q", a.Node, a.Verdict)
|
||||
case !refused && a.Verdict != ComposeCandidateComposed:
|
||||
return fmt.Errorf("delivery: %s's answer refuses no module and its verdict is %q", a.Node, a.Verdict)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// strictDecode reads data into v, refusing an unknown key.
|
||||
func strictDecode(data []byte, v any, what string) error {
|
||||
d := json.NewDecoder(bytes.NewReader(data))
|
||||
d.DisallowUnknownFields()
|
||||
d.UseNumber() // a number in an object the contract leaves open (the composition input) keeps its digits as written
|
||||
if err := d.Decode(v); err != nil {
|
||||
return fmt.Errorf("delivery: %s does not fit its shape: %w", what, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeComposeCandidateAsk is a as JSON; it refuses an ask naming no candidate key.
|
||||
func EncodeComposeCandidateAsk(a ComposeCandidateAsk) ([]byte, error) {
|
||||
if err := checkComposeAsk(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
// DecodeComposeCandidateAsk reads an ask strictly; it refuses an unknown key and what EncodeComposeCandidateAsk refuses.
|
||||
func DecodeComposeCandidateAsk(data []byte) (ComposeCandidateAsk, error) {
|
||||
var a ComposeCandidateAsk
|
||||
if err := strictDecode(data, &a, "a compose-candidate ask"); err != nil {
|
||||
return ComposeCandidateAsk{}, err
|
||||
}
|
||||
return a, checkComposeAsk(a)
|
||||
}
|
||||
|
||||
// EncodeComposeCandidateAnswer is a as JSON at Version; it refuses an unknown state, a composed module without a digest,
|
||||
// a refused or held one without words, a refusal of the whole naming modules, and a verdict the modules do not give.
|
||||
func EncodeComposeCandidateAnswer(a ComposeCandidateAnswer) ([]byte, error) {
|
||||
if err := checkComposeAnswer(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.V = Version
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
// DecodeComposeCandidateAnswer reads an answer strictly; it refuses an unknown version (ErrUnknownVersion), an unknown
|
||||
// key and what EncodeComposeCandidateAnswer refuses.
|
||||
func DecodeComposeCandidateAnswer(data []byte) (ComposeCandidateAnswer, error) {
|
||||
if err := checkVersion(data); err != nil {
|
||||
return ComposeCandidateAnswer{}, err
|
||||
}
|
||||
var a ComposeCandidateAnswer
|
||||
if err := strictDecode(data, &a, "a compose-candidate answer"); err != nil {
|
||||
return ComposeCandidateAnswer{}, err
|
||||
}
|
||||
return a, checkComposeAnswer(a)
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// A composition that carries its plan or its refusal (ADR 0593 §6): each round-trips, and a reader refuses what it could
|
||||
// not take, naming it. (compose-candidate's ask and answer are node-engine's client's, mesh-contracts
|
||||
// go/clients/nodeengineclient, ADR 0627 §1.)
|
||||
func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -11,6 +11,7 @@ catalogue/checker_seat.go: CheckerSeat
|
||||
catalogue/control_records_seats.go: HandActsSeat
|
||||
catalogue/control_records_seats.go: WantsSeat
|
||||
catalogue/delivery_seat.go: DeliverySeat
|
||||
catalogue/engine_seat.go: EngineSeat
|
||||
catalogue/enrolment_seat.go: EnrolmentSeat
|
||||
catalogue/environment_into.go: EnvironmentSeat
|
||||
catalogue/environment_into.go: LoginShellSeat
|
||||
|
||||
Reference in New Issue
Block a user