A module the catalogue no longer holds ends deleted instead of failing the plan; the unsent hint names release-backlog (issue 1922)

This commit is contained in:
2026-10-23 22:15:18 +02:00
parent 5831eec770
commit 12dd2bb3f0
3 changed files with 65 additions and 7 deletions
+6 -6
View File
@@ -328,8 +328,8 @@ func applied(module string, builtAt time.Time, running []string, reports []inven
// askTier asks the build machine for the named modules of the plan's tier — all of it, or what of it is still
// unasked (novox/hq issue 754) — and marks each asked. A module
// the catalogue no longer holds, or whose ask could not be made, is a failure of the plan: a tier
// half asked is a tier that will never complete.
// the catalogue no longer holds ends as deleted (issue 1922); one whose ask could not be made is a failure of the
// plan: a tier half asked is a tier that will never complete.
func askTier(ctx context.Context, inv *inventory.Inventory, p *inventory.Plan, names []string) error {
entries, err := inv.Catalogued(ctx)
if err != nil {
@@ -390,10 +390,10 @@ func askModule(ctx context.Context, p *inventory.Plan, name string, byName map[s
}
e, known := byName[name]
if !known {
state.State = "failed"
// **A module the catalogue no longer holds ends as deleted** (issue 1922): the plan's own merge removed it, so
// there is nothing to build or send; it is no failure, and a retry of a plan failed that way resumes.
state.State = planDeleted
state.Why = "no longer in the catalogue"
p.State = inventory.PlanFailed
p.Note = name + " is no longer in the catalogue"
return
}
// **Every module is taken from the builder's done key, asking no build** (novox/hq ADR 0503 §6, ADR 0501, ADR 0453 §5;
@@ -1647,7 +1647,7 @@ func sayUnsent(p *inventory.Plan, rollsOut func(string) bool) {
return
}
sort.Strings(unsent)
p.Note += "; built and never sent: " + strings.Join(unsent, ", ") + " — `push --behind` sends them"
p.Note += "; built and never sent: " + strings.Join(unsent, ", ") + " — `upgrade release-backlog --why <text>` releases them"
}
// planLine is one plan as `status` says it, given the least bound of a tier.
@@ -0,0 +1,58 @@
package main
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/inventory"
)
// 1922/AC1: a tier naming a module the catalogue no longer holds asks it without failing the plan: the module ends
// as deleted (not built, not sent) and the tier's other modules go on.
func TestAModuleTheCatalogueNoLongerHoldsEndsDeletedAndTheTierGoesOn(t *testing.T) {
p, byName := aWalkOfOne("sudo")
p.Tiers = [][]string{{"gone", "sudo"}}
for _, m := range p.Tiers[0] {
askModule(t.Context(), p, m, byName)
}
if g := p.Modules["gone"]; g == nil || g.State != planDeleted || g.Build != "" || g.SentAt != nil {
t.Fatalf("the removed module is not deleted: %+v", g)
}
if p.State == inventory.PlanFailed {
t.Fatalf("the plan failed: %q", p.Note)
}
if s := p.Modules["sudo"]; s == nil || s.State != "asked" {
t.Fatalf("the other module of the tier did not go on: %+v", s)
}
}
// 1922/AC2: a plan that failed on such a module before the fix is retried: the removed module ends deleted and the
// plan is building again, to go on to its next tier.
func TestARetryOfAPlanFailedOnARemovedModuleResumes(t *testing.T) {
p := inventory.Plan{ID: "plan-1", State: inventory.PlanFailed, Note: "gone is no longer in the catalogue",
Tiers: [][]string{{"gone"}, {"next"}},
Modules: map[string]*inventory.PlanModule{
"gone": {State: "failed", Why: "no longer in the catalogue"},
}}
failed := toRetry(&p, nil, nil)
if len(failed) != 1 || failed[0] != "gone" {
t.Fatalf("the failed set is %v", failed)
}
for _, m := range failed {
askModule(t.Context(), &p, m, map[string]inventory.Entry{})
}
resumed(&p, "retried")
if p.State != inventory.PlanBuilding || p.Modules["gone"].State != planDeleted {
t.Fatalf("the retry did not resume: %s %+v", p.State, p.Modules["gone"])
}
}
// 1922/AC3: an ended plan's note names the verb that releases the modules waiting for a gate, not `push --behind`.
func TestAnEndedPlansHintNamesTheReleaseOfTheBacklog(t *testing.T) {
p := inventory.Plan{State: inventory.PlanFailed, Note: "failed",
Modules: map[string]*inventory.PlanModule{"agent": {State: "built"}}}
sayUnsent(&p, func(string) bool { return true })
if strings.Contains(p.Note, "push --behind") || !strings.Contains(p.Note, "upgrade release-backlog") {
t.Fatalf("the note reads %q", p.Note)
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ func TestAnEndedPlanSaysWhatItBuiltAndNeverSent(t *testing.T) {
rollsOut := func(m string) bool { return m != "notes" }
sayUnsent(&p, rollsOut)
sayUnsent(&p, rollsOut)
if p.Note != "closed by hand at tier 1; built and never sent: agent — `push --behind` sends them" {
if p.Note != "closed by hand at tier 1; built and never sent: agent — `upgrade release-backlog --why <text>` releases them" {
t.Fatalf("the note reads %q", p.Note)
}
}