From cb7a444a5634e9d202df0cb250aa3f67e1366582 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 7 Nov 2026 08:19:55 +0100 Subject: [PATCH] The check's composer says a node's refusal in the words of its composition with the lifted modules held (issue 1921, milestone 47/G) When a node refused also with the modules composed without their build held as its node-engine holds them, the verdict kept the words of the composition from their unbuilt manifests, whose refusals the lift itself made (a tool runner with no bundle, a resource whose artifact or template no build resolved). In the group check of c5d32074c945 that hid lab's real refusal on shanks among them. The node's refusal is now that of the held composition, named so. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- internal/composer/composer.go | 10 ++++++-- internal/composer/composer_test.go | 37 +++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/composer/composer.go b/internal/composer/composer.go index 72e3db2..5b2682e 100644 --- a/internal/composer/composer.go +++ b/internal/composer/composer.go @@ -241,11 +241,17 @@ func runCase(r Request, c Case, depth string) (CaseVerdict, error) { // Composed without the build it runs at: named per node and module, never a silent pass (issue 1921). The // machine is then not judged, as for a node whose running state its engine does not send (ADR 0593 ยง9). Where // it does not compose without that build (a tool runner's bundle), it is composed with the module held as its - // node-engine holds it, and named so; a node that refuses then too is the change's refusal. + // node-engine holds it, and named so; a node that refuses then too is the change's refusal, in the words of that + // composition, never of the one from the unbuilt manifests, whose refusals the lift itself made (a tool runner + // with no bundle, a resource whose artifact or template no build resolved: the group check of c5d32074c945 on + // shanks). how := "composed from the change's manifest without its build: " if !m.Composes && m.NotJudged == "" { - if again := machine(heldAgain(in, held, awaited), node, depth, r.Declarations); again.Composes { + switch again := machine(heldAgain(in, held, awaited), node, depth, r.Declarations); { + case again.Composes: m, how = again, "composed with it held as its node-engine holds it, since it does not compose without its build: " + case again.NotJudged == "": + m, how = again, "refused with it held as its node-engine holds it, since it does not compose without its build: " } } words = how + words diff --git a/internal/composer/composer_test.go b/internal/composer/composer_test.go index 51bced2..1dc0792 100644 --- a/internal/composer/composer_test.go +++ b/internal/composer/composer_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" @@ -317,30 +318,50 @@ func TestARequestItCannotRunIsRefused(t *testing.T) { if err != nil { t.Fatal(err) } + runner := `{"module": "app", "version": "1", "tool-runner": true, ` + + `"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}` for _, row := range []struct { name, raw, digest string words string // in NotJudged, its build's paths not declared; "" composes at its build + beside string // a module the change refuses on the same node: words are then in the refusal }{ - {"the digest of the case's manifest", plain, same, ""}, - {"another digest, the same contract", plain, strings.Repeat("b", 64), "of another manifest than the change's (its build was made of the manifest " + strings.Repeat("b", 64)}, - {"no digest, the same contract", plain, "", ""}, + {"the digest of the case's manifest", plain, same, "", ""}, + {"another digest, the same contract", plain, strings.Repeat("b", 64), "of another manifest than the change's (its build was made of the manifest " + strings.Repeat("b", 64), ""}, + {"no digest, the same contract", plain, "", "", ""}, {"no digest, another contract", strings.Replace(plain, `"version": "1"`, `"version": "2"`, 1), "", - "app is composed by the running node-engine at its build fp1, of another manifest than the change's (its node-engine names no manifest"}, - {"a tool runner, its bundle awaited", `{"module": "app", "version": "1", "tool-runner": true, ` + - `"build": {"artifacts": [{"name": "runtime", "kind": "bundle", "language": "go", "from": "cmd/runner", "binary": "runner"}]}}`, "", - "composed with it held as its node-engine holds it, since it does not compose without its build: app is composed by the running node-engine at its build fp1"}, + "app is composed by the running node-engine at its build fp1, of another manifest than the change's (its node-engine names no manifest", ""}, + {"a tool runner, its bundle awaited", runner, "", + "composed with it held as its node-engine holds it, since it does not compose without its build: app is composed by the running node-engine at its build fp1", ""}, + // The group check of c5d32074c945, shanks: the change refused lab there, and the words were those of the + // composition from the change's manifests unbuilt ("mesh-build-go runs as its own account and its build + // produced no bundle to serve"), not of the one with them held, so the lift's own refusals failed the node. + {"a tool runner awaited beside a module the change refuses", runner, "", + "refused with it held as its node-engine holds it, since it does not compose without its build: app is composed by the running node-engine at its build fp1", + `{"module": "office", "version": "1", "resources": [{"id": "c", "type": "file", "path": "/etc/office.conf", "template": "x"}]}`}, } { in := compose.Input{Catalogue: map[string]catalogue.Manifest{"app": built}, Nodes: []compose.Node{{Name: "a", Assigned: []string{"app"}}}, ComposedHow: &compose.ComposedHow{Node: "a", Builds: map[string]string{"app": "fp1"}}} if row.digest != "" { in.ComposedHow.Manifests = map[string]string{"app": row.digest} } + manifests := map[string]Manifest{"app": {Raw: json.RawMessage(row.raw)}} + if row.beside != "" { + manifests["office"] = Manifest{Raw: json.RawMessage(row.beside)} + in.Nodes[0].Assigned = append(in.Nodes[0].Assigned, "office") + } v, err := Run(withInput(t, Request{V: Version, Machines: []string{"a"}, Depth: DepthCompose, Declarations: true, - Cases: []Case{{Name: "change", Manifests: map[string]Manifest{"app": {Raw: json.RawMessage(row.raw)}}}}}, in)) + Cases: []Case{{Name: "change", Manifests: manifests}}}, in)) if err != nil { t.Fatal(err) } m := v.Cases[0].Machines["a"] + if row.beside != "" { + said := fmt.Sprint(m.Reason, m.Problems, m.Refusals) + if m.Composes || m.NotJudged != "" || !strings.Contains(said, "office") || strings.Contains(said, "bundle") || !strings.Contains(m.Reason, row.words) { + t.Errorf("%s: machine %+v, want the change's refusal of office with app held, naming %q", row.name, m, row.words) + } + continue + } declared := false for _, p := range m.Declared { declared = declared || p.Path == "/var/lib/app-built"