From bb18c4d14fc33fa8ecd866b077cfe0d732b3c9b9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 00:27:55 +0100 Subject: [PATCH] Say why a key stays asked when its provider's helper reads no plans of the seat, so a provider left on the controller's assignments is named instead of waited on in silence (issue 774) --- .../cmd/mesh-provisioner/holder.go | 22 ++++++++++++++++++- .../cmd/mesh-provisioner/holder_test.go | 20 ++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go index f1c5c014..87f1460d 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder.go @@ -568,7 +568,7 @@ func (h *Holder) outcome(key string, p *provisions.Provision) { return } if e == nil { - p.State, p.Why = provisions.Asked, "" + p.State, p.Why = provisions.Asked, h.unread(p.Provider.Module) return } var en entry @@ -596,6 +596,26 @@ func (h *Holder) outcome(key string, p *provisions.Provision) { } } +// unread is why a provider whose helper keeps its outcome in its entries has written none, when its manifest says it: +// it reads no plans of the seat (novox/hq ADR 0570 clause 2), so its helper never hears a bind and the key stays asked +// for good (issue 774, 2026-10-31: route-proxy's helper read only the controller's assignments, and 38 routes the seat +// keyed for modules not handed stood asked with no why while the assigner withheld their consumers' builds). "" when +// it reads them, or its manifest cannot say: then it has not answered yet. +func (h *Holder) unread(module string) string { + manifest, err := h.providerManifest(module) + if err != nil { + return "" + } + var decl struct { + Reads []string `json:"reads"` + } + if json.Unmarshal([]byte(manifest), &decl) != nil || slices.Contains(decl.Reads, provisions.PlansRead) { + return "" + } + return module + " reads no plans of the seat (its manifest's reads do not name " + provisions.PlansRead + + "), so its helper never answers this key" +} + // haveOutcome is the outcome of a provider that makes no credential per consumer: its own have on its node (the lead's // reading of ADR 0570 clause 3, recorded as an insight). Running with health ok at the build its want names is // provided; failed is failed with the have's words; anything else is asked, waiting for the provider to run. diff --git a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go index 6cba2638..f4bb55ad 100644 --- a/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go +++ b/modules/mesh-provisioner/cmd/mesh-provisioner/holder_test.go @@ -443,31 +443,39 @@ func TestAProvidersOutcomeIsReadPerOffer(t *testing.T) { entry string have string // the provider's have key, written running state string + why string // a part of the provision's why, when it says one }{ {"gitea-read: the helper's refusal is not its outcome; gitea runs, so provided", "ace", map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "gitea-read", Provider: "novox", ProviderModule: "gitea"})}, - "ace.claude-code.gitea-read", "gitea.bound", refused, "novox.gitea", provisions.Provided}, + "ace.claude-code.gitea-read", "gitea.bound", refused, "novox.gitea", provisions.Provided, ""}, {"forge-account: still the helper's entry", "ace", map[string]catalogue.Plan{"mesh-build-go": planOf(catalogue.PlanProvision{Provision: "forge-account", Provider: "novox", ProviderModule: "gitea"})}, - "ace.mesh-build-go.forge-account", "gitea.bound", `{"state":"provided"}`, "", provisions.Provided}, + "ace.mesh-build-go.forge-account", "gitea.bound", `{"state":"provided"}`, "", provisions.Provided, ""}, {"route: route-proxy's entry is read", "novox", map[string]catalogue.Plan{"gitea": planOf(catalogue.PlanProvision{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"})}, - "novox.gitea.route", "route-proxy.bound", `{"state":"provided"}`, "", provisions.Provided}, + "novox.gitea.route", "route-proxy.bound", `{"state":"provided"}`, "", provisions.Provided, ""}, {"mcp-endpoint: node-tools' entry is read", "ace", map[string]catalogue.Plan{"claude-code": planOf(catalogue.PlanProvision{Provision: "mcp-endpoint", Provider: "ace", ProviderModule: "node-tools"})}, - "ace.claude-code.mcp-endpoint", "node-tools.bound", `{"state":"provided"}`, "", provisions.Provided}, + "ace.claude-code.mcp-endpoint", "node-tools.bound", `{"state":"provided"}`, "", provisions.Provided, ""}, + // Issue 774, 2026-10-31: route-proxy's helper read the controller's assignments, never the seat's plans, so 38 + // routes the seat keys stood asked with no why while the assigner withheld their consumers' builds. + {"route: no entry from a provider that reads no plans of the seat is said so", "ace", + map[string]catalogue.Plan{"grafana": planOf(catalogue.PlanProvision{Provision: "route", Provider: "ace", ProviderModule: "route-proxy"})}, + "ace.grafana.route", "", "", "", provisions.Asked, "reads no plans of the seat"}, } { m := newMesh() m.manifestOf("gitea", gitea) m.manifestOf("route-proxy", routes) m.manifestOf("node-tools", nodeTools) m.atWant[row.node], m.atNewest[row.node] = row.plan, row.plan - m.s.write(t, row.bucket, row.key, row.entry) + if row.bucket != "" { + m.s.write(t, row.bucket, row.key, row.entry) + } if row.have != "" { m.s.write(t, HaveRead, row.have, running) } m.compose(t, row.node) - if p := m.state(t, row.key); p.State != row.state { + if p := m.state(t, row.key); p.State != row.state || !strings.Contains(p.Why, row.why) { t.Errorf("%s: %s is %s (%s)", row.name, row.key, p.State, p.Why) } }