diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index ac832ac..eabb180 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -429,6 +429,17 @@ func input(ctx context.Context, r Reader, mesh Mesh, s Scope, refuse func(module // elsewhere, a stand-in for the node's own) refuses the node, err. func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]string) (out map[string]delivery.Composed, skipped []string, held map[string]string, err error) { held = map[string]string{} + // A node refused whole says what it held on the way, since the refusal can be of a held module's stub (issue 774, + // 2026-10-30 19:30: node-tools held, then "its build produced 0 bundle(s)" with its own refusal unsaid). + defer func() { + if err != nil && len(held) > 0 { + var said []string + for _, m := range sortedKeys(held) { + said = append(said, m+": "+held[m]) + } + err = fmt.Errorf("%w (held before: %s)", err, strings.Join(said, "; ")) + } + }() in, err := input(ctx, r, mesh, s, func(module string, err error) { held[module] = err.Error() }) if err != nil { return nil, nil, held, err diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index bb53330..09627ec 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -138,6 +138,8 @@ func TestInput(t *testing.T) { // saying why (answer_rows_test.go). t.Run("every refusal, and an old build left to the node", answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode) 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("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 new file mode 100644 index 0000000..aa8b79e --- /dev/null +++ b/go/compose/frombus/held_rows_test.go @@ -0,0 +1,40 @@ +package frombus + +import ( + "context" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// aNode is a bus with n1 the hub, each of manifests placed on it and held at the indexer, in the order given. +func aNode(t *testing.T, manifests ...string) (*bus, map[string]string) { + t.Helper() + p := delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"}} + b := &bus{records: map[string]delivery.NodeRecord{"n1": {Node: "n1", TunnelKey: publicKey(t), SealingKey: "sk", Capabilities: []string{"systemd"}}}, + modules: map[string]delivery.Module{}, manifests: map[string][]byte{}, builds: map[string]*delivery.BuilderBuild{}} + fps := map[string]string{} + for i, raw := range manifests { + module := strings.SplitN(strings.TrimPrefix(raw, `{"module":"`), `"`, 2)[0] + p.Modules = append(p.Modules, delivery.PlacedModule{Module: module}) + b.modules[module] = delivery.Module{Manifest: delivery.ManifestDigest([]byte(raw))} + b.manifests[delivery.ManifestDigest([]byte(raw))] = []byte(raw) + fps[module] = "src2:" + strings.Repeat(string(rune('1'+i)), 64) + } + b.placements = map[string]delivery.Placement{"n1": p} + return b, fps +} + +// Issue 774, 2026-10-30 19:30: a node refused whole after holding a module says what it held and why, since the last +// refusal can be of the held module's stub (node-tools held, then "its build produced 0 bundle(s)", its own why unsaid). +func aNodeRefusedWholeSaysWhatItHeld(t *testing.T) { + refused := `{"module":"power","version":"1","items":[{"collection":"contribution:node-bar:block","attributes":{"shows":"state"},"template":"files/a"}]}` + needs := `{"module":"lamp","version":"1","requires":["nothing-provides-this"]}` + b, fps := aNode(t, refused, needs) + _, _, held, err := Compose(context.Background(), b, Mesh{}, Scope{Node: "n1"}, fps) + if err == nil || !strings.Contains(err.Error(), "nothing provides") || !strings.Contains(err.Error(), `held before: power: `) || + !strings.Contains(err.Error(), `"shows" is none of them`) { + t.Errorf("the node refused whole with %v (held %v), want lamp's refusal and power held before, with its why", err, held) + } +}