From d539d7e4eb2571e9bf32709f257f6e9806cbe455 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 19:44:08 +0100 Subject: [PATCH] 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. --- go/compose/frombus/answer.go | 12 +++++++++++- go/compose/frombus/frombus_test.go | 1 + go/compose/frombus/held_rows_test.go | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/go/compose/frombus/answer.go b/go/compose/frombus/answer.go index fc57937..74f7337 100644 --- a/go/compose/frombus/answer.go +++ b/go/compose/frombus/answer.go @@ -373,6 +373,16 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b 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 // what it contributes to that provision alone. 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 { _, _, _, 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 { if _, done := failed[m]; done { diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index 09627ec..7dc9ab5 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -140,6 +140,7 @@ func TestInput(t *testing.T) { t.Run("no store reached says why", noStoreReachedSaysWhy) // 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("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes) t.Run("another node not resolved is named", otherNodeNotResolvedIsNamed) ctx := context.Background() xterm, err := os.ReadFile("../../catalogue/testdata/catalogue-835bf22b-xterm.module.json") diff --git a/go/compose/frombus/held_rows_test.go b/go/compose/frombus/held_rows_test.go index aa8b79e..fc3438e 100644 --- a/go/compose/frombus/held_rows_test.go +++ b/go/compose/frombus/held_rows_test.go @@ -2,9 +2,11 @@ package frombus import ( "context" + "errors" "strings" "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "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) } } + +// 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) + } + } +}