From 2377d75ed89840b51f7cc21f70267858b7ce5150 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 18:57:59 +0200 Subject: [PATCH] Say a building plan's refusal in its line, not 'building' with LATE, once its tier is built (issue 920) --- cmd/mesh-controller/plan_clock_test.go | 31 ++++++++++++++++++++++++++ cmd/mesh-controller/release_plan.go | 21 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/cmd/mesh-controller/plan_clock_test.go b/cmd/mesh-controller/plan_clock_test.go index 49631e53..ef4b8156 100644 --- a/cmd/mesh-controller/plan_clock_test.go +++ b/cmd/mesh-controller/plan_clock_test.go @@ -78,3 +78,34 @@ func TestAPlansLineCountsFromItsTierNotFromItsLastSave(t *testing.T) { // novox/hq issue 296: a plan whose tier stands still is not written again by every advance — its revision, // its last save and its `plan-moved` stay where its last change left them. + +// novox/hq issue 920: a building plan whose current tier is all built waits on its note (a send refused at the +// gate), and its line, `status` and the stalled condition say that, not "building for ... LATE". +func TestAPlanLineSaysTheRefusalOnceItsTierIsBuilt(t *testing.T) { + now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC) + built := now.Add(-time.Hour) + refusal := "tier 1: a build waits there for its gate: novox (mesh-delivery-indexer d68d7ef3→a5aa69a4) — tried again" + mk := func(second string) inventory.Plan { + return inventory.Plan{ID: "plan-r", Repository: "novox/mesh-sdk", Commit: "b2f5ebb2", State: inventory.PlanBuilding, + Tier: 1, Created: now.Add(-2 * time.Hour), Updated: now, TierEntered: now.Add(-time.Hour), Note: refusal, + Tiers: [][]string{{"a"}, {"b", "c"}}, + Modules: map[string]*inventory.PlanModule{"a": {State: "built", BuiltAt: &built}, + "b": {State: "built", BuiltAt: &built}, "c": {State: second}}} + } + + // 920/AC1 + p := mk("built") + line := planLine(p, now) + if !strings.Contains(line, "a build waits there for its gate: novox") || strings.Contains(line, "building for") { + t.Errorf("920/AC1: a plan with its tier built and a refusal reads %q", line) + } + st := planStatuses([]inventory.Plan{p}, now, pauseView{}, nil) + if len(st) != 1 || !st[0].Late { + t.Errorf("920/AC1: status: %+v", st) + } + + // 920/AC2 + if line := planLine(mk("asked"), now); !strings.Contains(line, "building for 1h0m0s") { + t.Errorf("920/AC2: a plan with a tier module not built reads %q", line) + } +} diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 7331626c..5ca93c4e 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -863,6 +863,20 @@ func deferred(p inventory.Plan) bool { p.Delivery.Awaits != "" && p.Delivery.Go != nil && strings.HasPrefix(p.Note, deferredNote) } +// currentTierBuilt says every module of a plan's current tier is built (or deleted), so no build is what it waits on. +func currentTierBuilt(p inventory.Plan) bool { + if p.Tier < 0 || p.Tier >= len(p.Tiers) || len(p.Tiers[p.Tier]) == 0 { + return false + } + for _, m := range p.Tiers[p.Tier] { + s := p.Modules[m] + if s == nil || (s.State != "built" && s.State != planDeleted) { + return false + } + } + return true +} + // askedAny says a plan asked any module. func askedAny(p inventory.Plan) bool { for _, s := range p.Modules { @@ -2025,6 +2039,13 @@ func planLineSaid(p inventory.Plan, now time.Time, pause pauseView, bound time.D if p.State == inventory.PlanRolling { what = p.Note } + if p.State == inventory.PlanBuilding && p.Note != "" && currentTierBuilt(p) { + // Every build of the tier is in, so the plan waits on something else, and its note says what: a send + // refused ("a build waits there for its gate: (...)") is kept there. "building" with LATE + // hid that reason from `plans`, `status` and the stalled condition (novox/hq issue 920). + what = p.Note + return fmt.Sprintf("%s · %s, held: %s, for %s%s", name, where, what, since, late) + } return fmt.Sprintf("%s · %s, %s for %s%s", name, where, what, since, late) }