From d158ac52dabd599e5ab5640c1e2736c397d53ac5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 18:20:05 +0100 Subject: [PATCH] =?UTF-8?q?composer:=20a=20request=20carries=20its=20compo?= =?UTF-8?q?sition=20input=20as=20raw=20JSON,=20so=20the=20shape=20stays=20?= =?UTF-8?q?in=20mesh-sdk=20while=20the=20composer=20moves=20to=20the=20nod?= =?UTF-8?q?e-engine=20(ADR=200593=20=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/cmd/mesh-compose/main_test.go | 13 +++++++++++-- go/composer/composer.go | 13 ++++++++++--- go/composer/composer_test.go | 18 ++++++++++++++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/go/cmd/mesh-compose/main_test.go b/go/cmd/mesh-compose/main_test.go index f2d909b..48d4b77 100644 --- a/go/cmd/mesh-compose/main_test.go +++ b/go/cmd/mesh-compose/main_test.go @@ -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 +} diff --git a/go/composer/composer.go b/go/composer/composer.go index 164a096..566f6ac 100644 --- a/go/composer/composer.go +++ b/go/composer/composer.go @@ -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) } diff --git a/go/composer/composer_test.go b/go/composer/composer_test.go index e1ec9ea..82a3de0 100644 --- a/go/composer/composer_test.go +++ b/go/composer/composer_test.go @@ -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 +}