composer: a request carries its composition input as raw JSON, so the shape stays in mesh-sdk while the composer moves to the node-engine (ADR 0593 §7)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 18:20:05 +01:00
parent 346dd43228
commit d158ac52da
3 changed files with 35 additions and 9 deletions
+11 -2
View File
@@ -46,7 +46,7 @@ func meshOf() compose.Input {
func request(t *testing.T, depth string) composer.Request {
t.Helper()
return composer.Request{V: composer.Version, Input: meshOf(), Depth: depth, HoldingsFromClaims: true,
return composer.Request{V: composer.Version, Input: rawInput(meshOf()), Depth: depth, HoldingsFromClaims: true,
Machines: []string{"anchor", "laptop", "ace"},
Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
}
@@ -180,7 +180,7 @@ func TestACatalogueCheckoutIsComposedForANode(t *testing.T) {
if err := os.WriteFile(filepath.Join(dir, "modules", "demo", "module.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
in := encode(t, composer.Request{V: composer.Version, Input: compose.Input{Nodes: []compose.Node{{Name: "laptop", Assigned: []string{"demo"}}}}})
in := encode(t, composer.Request{V: composer.Version, Input: rawInput(compose.Input{Nodes: []compose.Node{{Name: "laptop", Assigned: []string{"demo"}}}})})
code, v, stderr := runJSON(t, []string{"-catalogue", dir, "-node", "laptop", "-declarations"}, in)
if code != 0 {
t.Fatalf("exit %d: %s", code, stderr)
@@ -318,3 +318,12 @@ func TestTheCommandRunsWithNoNetwork(t *testing.T) {
sort.Strings(names)
t.Logf("composed with no network: %v", names)
}
// rawInput is a composition input as a request carries it.
func rawInput(in compose.Input) json.RawMessage {
raw, err := json.Marshal(in)
if err != nil {
panic(err)
}
return raw
}
+10 -3
View File
@@ -44,8 +44,9 @@ const (
type Request struct {
V int `json:"v"`
// Input is the composition's input without its catalogue (and without each node's kept manifests): the cases give
// those. A Catalogue or Kept it holds is ignored.
Input compose.Input `json:"input"`
// those. A Catalogue or Kept it holds is ignored. It is the node-engine's composition input as JSON, which only the
// composer decodes (novox/hq ADR 0593 §7), so this shape imports no composition.
Input json.RawMessage `json:"input"`
// Cases are composed one after the other over Input, each with its own manifests; their names are unique.
Cases []Case `json:"cases"`
// Machines are the nodes composed in every case; none means the manifests are only read.
@@ -267,7 +268,13 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) {
if err != nil {
return CaseVerdict{}, err
}
in, err := frombus.Placed(r.Input, c.Placements)
var input compose.Input
if len(r.Input) > 0 {
if err := json.Unmarshal(r.Input, &input); err != nil {
return CaseVerdict{}, fmt.Errorf("composer: the request's input is not a composition input: %w", err)
}
}
in, err := frombus.Placed(input, c.Placements)
if err != nil {
return CaseVerdict{}, fmt.Errorf("composer: the case %q: %w", c.Name, err)
}
+14 -4
View File
@@ -64,7 +64,7 @@ func TestTheInputTravelsAsJSON(t *testing.T) {
Holdings: []catalogue.Held{{Claim: "mesh-broker", Scope: "mesh", Node: "anchor", Module: "nats"}},
Issued: []compose.Issued{{Provision: "bus", Consumer: "laptop", ConsumerModule: "x", Provider: "anchor"}},
}
raw, err := json.Marshal(Request{V: Version, Input: in})
raw, err := json.Marshal(Request{V: Version, Input: rawInput(in)})
if err != nil {
t.Fatal(err)
}
@@ -72,8 +72,9 @@ func TestTheInputTravelsAsJSON(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(back.Input, in) {
t.Fatalf("the input came back as\n%+v\nwant\n%+v", back.Input, in)
var got compose.Input
if err := json.Unmarshal(back.Input, &got); err != nil || !reflect.DeepEqual(got, in) {
t.Fatalf("the input came back as\n%+v (%v)\nwant\n%+v", got, err, in)
}
}
@@ -102,7 +103,7 @@ func TestARequestItCannotRunIsRefused(t *testing.T) {
t.Error("an unknown depth runs")
}
// A case's own placement (issue 1126) composes over a node the input holds, and is refused for one it does not.
placed := Request{V: Version, Input: compose.Input{Nodes: []compose.Node{{Name: "a"}}}, Machines: []string{"a"}, Depth: DepthResolve,
placed := Request{V: Version, Input: rawInput(compose.Input{Nodes: []compose.Node{{Name: "a"}}}), Machines: []string{"a"}, Depth: DepthResolve,
Cases: []Case{{Name: "trunk", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}}},
{Name: "change", Manifests: map[string]Manifest{"demo": {Raw: json.RawMessage(demo)}},
Placements: map[string]*delivery.Placement{"a": {Node: "a", Modules: []delivery.PlacedModule{{Module: "ghost"}}}}}}}
@@ -152,3 +153,12 @@ func TestTheComposerTakesTheVerdictsOfItsInput(t *testing.T) {
t.Fatalf("xterm refused by its verdict: %+v, want refused in the verdict's words", got)
}
}
// rawInput is a composition input as a request carries it.
func rawInput(in compose.Input) json.RawMessage {
raw, err := json.Marshal(in)
if err != nil {
panic(err)
}
return raw
}