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)
mesh/delivery merged: source.main moved to novox/mesh-host@70e70eda5a24
mesh/delivery-group group feat/registry-milestone merged: every member is merged
mesh/check checking cb7a444a5634 on 70e70eda5a24 | novox/mesh-host#209 in group feat/registry-milestone

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
This commit is contained in:
2026-11-07 08:19:55 +01:00
parent 53addd73c3
commit cb7a444a56
2 changed files with 37 additions and 10 deletions
+8 -2
View File
@@ -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
+29 -8
View File
@@ -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"