An input carries the modules it holds as their stubs across the bus, and the sandbox names why the node-engine holds one (issue 774, group check 278aa5f0ccbe)
The stub's mark (Manifest.Stubbed) is no field of a manifest's JSON, so an input a node-engine sent the check's sandbox lost it: node-tools, held by its manifest's verdict, read as a tool runner with no bundle and refused every module of ace, g14 and shanks for "its build produced 0 bundle(s)". compose.Input now writes its held modules (catalogue and each node's kept) beside its fields and marks them again on reading; an input from an engine before the mark is read by its contract, every module Refused names held as its stub. The sandbox composer also says, in the module's read, why the input's node-engine holds a module the case carries, so the check names the cause, not a rule about the stub. One subtest round-trips an input holding a refused tool runner, in both shapes; it refused for 0 bundles before. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// What an input says of its held modules on the wire (issue 774, the group check of feat/l3-final 278aa5f0ccbe). A held
|
||||
// module stands in the input as its stub (Stub), and the stub's mark (catalogue.Manifest.Stubbed) is no field of a
|
||||
// manifest's JSON: an input a node-engine sent the check's sandbox lost it, so the sandbox read node-tools, held by its
|
||||
// manifest's verdict, as a tool runner with no bundle and refused every module of ace, g14 and shanks for "its build
|
||||
// produced 0 bundle(s)" instead of naming the verdict. So the input carries the mark itself, by module, for its catalogue
|
||||
// and for each node's kept manifests, and a reader marks each again. An input from a node-engine before the mark (no
|
||||
// Held) is read by its contract: a module Refused names stands in Catalogue as its stub.
|
||||
|
||||
// heldMarks are the modules an input holds as their stubs: those of its catalogue, and those each node keeps, by node.
|
||||
type heldMarks struct {
|
||||
Catalogue []string `json:",omitempty"`
|
||||
Kept map[string][]string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// inputFields is Input without its JSON methods.
|
||||
type inputFields Input
|
||||
|
||||
// inputWire is an input as it crosses the bus: its fields, and the modules it holds as their stubs.
|
||||
type inputWire struct {
|
||||
inputFields
|
||||
Held *heldMarks `json:",omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON writes the input with the modules it holds as their stubs named beside it (heldMarks).
|
||||
func (in Input) MarshalJSON() ([]byte, error) {
|
||||
w := inputWire{inputFields: inputFields(in)}
|
||||
marks := heldMarks{Catalogue: stubbedOf(in.Catalogue)}
|
||||
for _, n := range in.Nodes {
|
||||
if kept := stubbedOf(n.Kept); len(kept) > 0 {
|
||||
if marks.Kept == nil {
|
||||
marks.Kept = map[string][]string{}
|
||||
}
|
||||
marks.Kept[n.Name] = kept
|
||||
}
|
||||
}
|
||||
if len(marks.Catalogue) > 0 || len(marks.Kept) > 0 {
|
||||
w.Held = &marks
|
||||
}
|
||||
return json.Marshal(w)
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads an input and marks again each module it holds as its stub: those it names held, and, by the
|
||||
// input's contract, every module Refused names that its catalogue holds.
|
||||
func (in *Input) UnmarshalJSON(raw []byte) error {
|
||||
var w inputWire
|
||||
if err := json.Unmarshal(raw, &w); err != nil {
|
||||
return err
|
||||
}
|
||||
*in = Input(w.inputFields)
|
||||
held := map[string]bool{}
|
||||
for m := range in.Refused {
|
||||
held[m] = true
|
||||
}
|
||||
if w.Held != nil {
|
||||
for _, m := range w.Held.Catalogue {
|
||||
held[m] = true
|
||||
}
|
||||
}
|
||||
for m := range held {
|
||||
if man, ok := in.Catalogue[m]; ok {
|
||||
man.Stubbed = true
|
||||
in.Catalogue[m] = man
|
||||
}
|
||||
}
|
||||
if w.Held != nil {
|
||||
for i := range in.Nodes {
|
||||
for _, m := range w.Held.Kept[in.Nodes[i].Name] {
|
||||
if man, ok := in.Nodes[i].Kept[m]; ok {
|
||||
man.Stubbed = true
|
||||
in.Nodes[i].Kept[m] = man
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stubbedOf is each module of manifests held as its stub, sorted.
|
||||
func stubbedOf(manifests map[string]catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for name, m := range manifests {
|
||||
if m.Stubbed {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -150,6 +150,14 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
|
||||
for name, m := range base.Catalogue {
|
||||
shelf[name] = m
|
||||
}
|
||||
// A module the input's node-engine holds as refused (compose.Input.Refused: its verdict, or bytes that do not decode) is
|
||||
// held here as there, and its read says why, whatever the case's own verdicts: the check then names the cause the node
|
||||
// holds it for, never a pass the node would not compose, nor a rule about its stub (issue 774, group check 278aa5f0ccbe).
|
||||
for name, why := range base.Refused {
|
||||
if read, carried := cv.Manifests[name]; carried && read.Accepted {
|
||||
cv.Manifests[name] = ManifestRead{Refused: "its node-engine holds it: " + why}
|
||||
}
|
||||
}
|
||||
in.Catalogue = shelf
|
||||
in.Nodes = slices.Clone(in.Nodes)
|
||||
in.Holdings = slices.Clone(in.Holdings)
|
||||
|
||||
@@ -179,6 +179,47 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
|
||||
t.Errorf("a module the node-engine holds too: %+v", m)
|
||||
}
|
||||
})
|
||||
// Issue 774 (the group check of feat/l3-final 278aa5f0ccbe): a tool runner its node-engine holds by its manifest's
|
||||
// verdict is sent as its stub, and the input says it holds it: the sandbox composes the node with it held, never refuses
|
||||
// it for its stub's missing bundle. An input from an engine before the mark is read by Refused alone, the same way.
|
||||
t.Run("a refused tool runner the engine sends as its stub", func(t *testing.T) {
|
||||
raw := `{"module": "runner", "version": "1", "tool-runner": true, ` +
|
||||
`"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`
|
||||
man, err := catalogue.Decode([]byte(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in := compose.Input{Catalogue: map[string]catalogue.Manifest{"runner": compose.Stub(man)},
|
||||
Refused: map[string]string{"runner": "the indexer's judge refused the manifest of runner"},
|
||||
Nodes: []compose.Node{{Name: "a", Assigned: []string{"runner"}}}}
|
||||
wire, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var back compose.Input
|
||||
if err := json.Unmarshal(wire, &back); err != nil || !back.Catalogue["runner"].Stubbed || !strings.Contains(string(wire), `"Held"`) {
|
||||
t.Fatalf("the input's held mark across the wire: %s, %v", wire, err)
|
||||
}
|
||||
older := strings.Replace(string(wire), `"Held":{"Catalogue":["runner"]}`, `"Unsaid":null`, 1)
|
||||
for name, sent := range map[string]string{"with the mark": string(wire), "from an engine before it": older} {
|
||||
var r Request
|
||||
r = Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose,
|
||||
Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"runner": {Raw: json.RawMessage(raw)}}}}}
|
||||
if err := json.Unmarshal([]byte(sent), &r.Input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v, err := Run(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m := v.Cases[0].Machines["a"]; !m.Composes || strings.Contains(m.Reason, "bundle") {
|
||||
t.Errorf("%s: the node with its tool runner held: %+v", name, m)
|
||||
}
|
||||
if read := v.Cases[0].Manifests["runner"]; read.Accepted || !strings.Contains(read.Refused, "the indexer's judge refused the manifest of runner") {
|
||||
t.Errorf("%s: the runner's read names not why its node-engine holds it: %+v", name, read)
|
||||
}
|
||||
}
|
||||
})
|
||||
v, err := Run(Request{V: Version, Cases: []Case{{Name: "only-read", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}}}})
|
||||
if err != nil || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted || v.Cases[0].Machines != nil || v.SDK == "" {
|
||||
t.Fatalf("a request with no machines: %+v, %v", v, err)
|
||||
|
||||
Reference in New Issue
Block a user