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)
mesh/delivery merged: source.main moved to novox/mesh-sdk@6a71d29b3331
mesh/delivery-group group fix/kept-read-and-foreseen-account merged: every member is merged
mesh/check pass 4e9621edd664 on 14de8dadb4ce | unit 2719 run, 602 kept, 36 skipped | integration 2731 run, 645 kept, 29 skipped | over bound: unit, in…

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 ("<m> is held: <why>; its stand-in is then refused: <err>"), for Compose and
the node's Answer alike; the hold's own why already keeps its first words. Chose thorough.
This commit is contained in:
2026-10-30 20:33:54 +01:00
parent 0cdbdca139
commit 4e9621edd6
3 changed files with 39 additions and 1 deletions
+18 -1
View File
@@ -335,7 +335,7 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
if err != nil { if err != nil {
module, leave := culprit(in, node, fps, mode, failed, err) module, leave := culprit(in, node, fps, mode, failed, err)
if module == "" { if module == "" {
return nil, nil, nil, err return nil, nil, nil, firstReason(err, failed)
} }
hold(module, err.Error(), leave) hold(module, err.Error(), leave)
continue continue
@@ -373,6 +373,23 @@ func composeEach(in compose.Input, node string, fps map[string]string, applied b
return out, skipped, stood, nil 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 // 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 // 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). // the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it).
+1
View File
@@ -142,6 +142,7 @@ func TestInput(t *testing.T) {
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("the runtime is blamed only where its stub composes", theRuntimeIsBlamedOnlyWhereItsStubComposes)
t.Run("a running module waits for its build", aRunningModuleWaitsForItsBuild) 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) 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")
+20
View File
@@ -131,3 +131,23 @@ func rawStrings(rs []json.RawMessage) []string {
} }
return out 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)
}
}
}