Merge pull request 'The provisioner says why a key stays asked when its provider reads no plans of the seat (issue 774)' (#575) from fix/holder-says-unread-plans into main

This commit was merged in pull request #575.
This commit is contained in:
2026-10-30 23:43:14 +00:00
2 changed files with 35 additions and 7 deletions
@@ -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.
@@ -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)
}
}