diff --git a/go/catalogue/engine_compose_candidate_test.go b/go/catalogue/engine_compose_candidate_test.go new file mode 100644 index 0000000..fb34000 --- /dev/null +++ b/go/catalogue/engine_compose_candidate_test.go @@ -0,0 +1,19 @@ +package catalogue + +import "testing" + +// The node-engine's compose-candidate verb (ADR 0593 §7) is optional, asks the candidate, and takes input as a switch. +func TestTheEngineServesComposeCandidate(t *testing.T) { + for _, v := range EngineVerbs() { + if v.Name != "compose-candidate" { + continue + } + props, _ := v.Input["properties"].(map[string]any) + input, _ := props["input"].(map[string]any) + if !v.Optional || props["candidate"] == nil || input == nil || input["enum"] == nil { + t.Fatalf("compose-candidate reads as %+v", v) + } + return + } + t.Fatal("the seat node-engine serves no compose-candidate") +} diff --git a/go/catalogue/engine_seat.go b/go/catalogue/engine_seat.go index 518e8e7..e5b6e3c 100644 --- a/go/catalogue/engine_seat.go +++ b/go/catalogue/engine_seat.go @@ -76,7 +76,9 @@ func EngineVerbs() []Verb { "composes (with its digest and whether the candidate changes it), is refused by the candidate or is held " + "without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: " + "applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", - Input: schema(map[string]string{"candidate": "the key of the indexer's candidate, ."}, []string{"candidate"})}, + Input: schema(map[string]string{"candidate": "the key of the indexer's candidate, .", + "input": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path"}, + []string{"candidate"}, "input")}, } } diff --git a/go/delivery/compose_candidate.go b/go/delivery/compose_candidate.go index ad382a8..25d2a17 100644 --- a/go/delivery/compose_candidate.go +++ b/go/delivery/compose_candidate.go @@ -27,6 +27,10 @@ const ( // 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" // 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. @@ -47,7 +51,7 @@ type ComposeCandidateAsk struct { // 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), how long the engine took in milliseconds, and, when asked +// 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. type ComposeCandidateAnswer struct { V int `json:"v"` @@ -111,9 +115,13 @@ func (a ComposeCandidateAnswer) check() error { if a.Took < 0 { return fmt.Errorf("delivery: the answer of %s took %d ms", a.Node, a.Took) } + if a.Verdict == ComposeCandidateUnready && a.Refused == "" { + return fmt.Errorf("delivery: %s is not ready to compose and its answer says no why", a.Node) + } if a.Refused != "" { - if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused { - return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or no refusal", a.Node, a.Refused) + if len(a.Modules) > 0 || a.Verdict != ComposeCandidateRefused && a.Verdict != ComposeCandidateUnready { + return fmt.Errorf("delivery: %s could not compose the candidate (%s), and its answer names modules or is neither %s nor %s", + a.Node, a.Refused, ComposeCandidateRefused, ComposeCandidateUnready) } return nil } diff --git a/go/delivery/compose_candidate_test.go b/go/delivery/compose_candidate_test.go index b7d069d..196ac89 100644 --- a/go/delivery/compose_candidate_test.go +++ b/go/delivery/compose_candidate_test.go @@ -64,6 +64,20 @@ func TestComposeCandidateAndComposedPlanRoundTripAndRefuse(t *testing.T) { 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") + } 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") }