From 4e9621edd664a8150c495596484ef8e15bdd787b Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 20:33:54 +0100 Subject: [PATCH] Lead a refusal of a held module's stand-in with why the module was held, so its first reason is never hidden (issue 774) The proposal on ace said only "mesh-self-check runs as its own account and declares no own secret broker": the words of its bare stub, held for "has no bus credential", which no one saw. The second hidden first reason of the day, after "0 bundle(s)". Quick fix: log the held map in composeEach. Thorough fix: firstReason leads a refusal no culprit is found for with the first reason of every held module it names (" is held: ; its stand-in is then refused: "), for Compose and the node's Answer alike; the hold's own why already keeps its first words. Chose thorough. --- go/compose/frombus/answer.go | 19 ++++++++++++++++++- go/compose/frombus/frombus_test.go | 1 + go/compose/frombus/held_rows_test.go | 20 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/go/compose/frombus/answer.go b/go/compose/frombus/answer.go index dcdb2f2..1f56daf 100644 --- a/go/compose/frombus/answer.go +++ b/go/compose/frombus/answer.go @@ -335,7 +335,7 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b if err != nil { module, leave := culprit(in, node, fps, mode, failed, err) if module == "" { - return nil, nil, nil, err + return nil, nil, nil, firstReason(err, failed) } hold(module, err.Error(), leave) continue @@ -373,6 +373,23 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b return out, skipped, stood, nil } +// firstReason is err, a refusal no culprit is found for, led by the first reason of each held module it names: a held +// module's stand-in refused later never replaces why it was held (issue 774, 2026-10-30: mesh-self-check on ace held for +// "has no bus credential", then refused as "runs as its own account and declares no own secret broker", its bare stub's +// words, which were all the proposal's refusal said). +func firstReason(err error, failed map[string]string) error { + var named []string + for _, m := range sortedKeys(failed) { + if strings.Contains(err.Error(), m) { + named = append(named, m+" is held: "+failed[m]) + } + } + if len(named) == 0 { + return err + } + return fmt.Errorf("%s; its stand-in is then refused: %w", strings.Join(named, "; "), err) +} + // 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). diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index b77fa2f..4cda26e 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -142,6 +142,7 @@ func TestInput(t *testing.T) { 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("a running module waits for its build", aRunningModuleWaitsForItsBuild) + t.Run("a held module's first reason leads", aHeldModulesFirstReasonLeads) 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 4deb0ce..491929d 100644 --- a/go/compose/frombus/held_rows_test.go +++ b/go/compose/frombus/held_rows_test.go @@ -131,3 +131,23 @@ func rawStrings(rs []json.RawMessage) []string { } return out } + +// Issue 774: a refusal of a held module's stand-in leads with why the module was held, its first reason, as mesh-self-check's +// on ace should have; a refusal naming no held module is as it was. +func aHeldModulesFirstReasonLeads(t *testing.T) { + held := map[string]string{"mesh-self-check": "mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check"} + for _, row := range []struct { + err, want string + }{ + {"mesh-self-check runs as its own account and declares no own secret broker", + "mesh-self-check is held: mesh-self-check on ace has no bus credential: nothing was issued for ace.mesh-self-check; " + + "its stand-in is then refused: mesh-self-check runs as its own account and declares no own secret broker"}, + {"ace is the hub and has no endpoint", "ace is the hub and has no endpoint"}, + } { + err := errors.New(row.err) + got := firstReason(err, held) + if got.Error() != row.want || !errors.Is(got, err) { + t.Errorf("refused %q, want %q", got, row.want) + } + } +}