Take the runtime as a culprit only where its stub composes, so node-tools is never held for an error that names it (issue 774)

The culprit rule held a module whose stub made the composition fail otherwise. The runtime's stub carries no bundle,
so it always fails otherwise: any refusal naming node-tools (or a module whose name holds it) held the runtime, and
the node was then refused whole for the runtime's "0 bundle(s)".

Quick fix: never take node-tools as a culprit. Thorough fix: blames, the rule as one function, takes the runtime only
where its stub composes. Chose thorough: a runtime whose stub composes may still be held alone.
This commit is contained in:
2026-10-30 19:44:08 +01:00
parent c1bc21c35f
commit d539d7e4eb
3 changed files with 35 additions and 1 deletions
+11 -1
View File
@@ -373,6 +373,16 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
return out, skipped, stood, nil return out, skipped, stood, nil
} }
// blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The
// runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is
// the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it).
func blames(m string, again, err error) bool {
if m == catalogue.RuntimeModule {
return again == nil
}
return again == nil || again.Error() != err.Error()
}
// leaveAll leaves out the whole of a held module's offer and its own secrets (a bare stub); any other word leaves out // leaveAll leaves out the whole of a held module's offer and its own secrets (a bare stub); any other word leaves out
// what it contributes to that provision alone. // what it contributes to that provision alone.
const leaveAll = "*" const leaveAll = "*"
@@ -419,7 +429,7 @@ func culprit(in compose.Input, node string, fps map[string]string, mode compose.
} }
changes := func(m, leave string) bool { changes := func(m, leave string) bool {
_, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode) _, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode)
return again == nil || again.Error() != err.Error() return blames(m, again, err)
} }
for _, m := range order { for _, m := range order {
if _, done := failed[m]; done { if _, done := failed[m]; done {
+1
View File
@@ -140,6 +140,7 @@ func TestInput(t *testing.T) {
t.Run("no store reached says why", noStoreReachedSaysWhy) t.Run("no store reached says why", noStoreReachedSaysWhy)
// Issue 774: a node refused whole says what it held on the way. // Issue 774: a node refused whole says what it held on the way.
t.Run("a node refused whole says what it held", aNodeRefusedWholeSaysWhatItHeld) t.Run("a node refused whole says what it held", aNodeRefusedWholeSaysWhatItHeld)
t.Run("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes)
t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed) t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed)
ctx := context.Background() ctx := context.Background()
xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json")
+23
View File
@@ -2,9 +2,11 @@ package frombus
import ( import (
"context" "context"
"errors"
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery"
) )
@@ -38,3 +40,24 @@ func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) {
t.Errorf("the node refused whole with %v (held %v), want lamp's refusal and power held before, with its why", err, held) t.Errorf("the node refused whole with %v (held %v), want lamp's refusal and power held before, with its why", err, held)
} }
} }
// Issue 774: the culprit rule takes a module whose stub composes or fails otherwise; the runtime only where its stub
// composes, since its stub always fails otherwise.
func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) {
err := errors.New("lamp refuses")
for _, row := range []struct {
module string
again error
blamed bool
}{
{"lamp", nil, true},
{"lamp", errors.New("other"), true},
{"lamp", errors.New("lamp refuses"), false},
{catalogue.RuntimeModule, nil, true},
{catalogue.RuntimeModule, errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false},
} {
if got := blames(row.module, row.again, err); got != row.blamed {
t.Errorf("%s with %v: blamed %v, want %v", row.module, row.again, got, row.blamed)
}
}
}