diff --git a/cmd/mesh-controller/delivery_conditions.go b/cmd/mesh-controller/delivery_conditions.go index 21d7ed94..942bd9aa 100644 --- a/cmd/mesh-controller/delivery_conditions.go +++ b/cmd/mesh-controller/delivery_conditions.go @@ -47,10 +47,14 @@ type stalledLine struct { // issue 443): a time inside a finished sentence cannot be said again in another zone. A mesh-delivery from before // says none, and Waiting is said as it reads. Checks []waitingCheck `json:"checks,omitempty"` - For string `json:"for"` - Bound string `json:"bound"` - H2 string `json:"h2"` - Says string `json:"says"` + // Held is the state of the delivery of a line of the state `unanswered`, ready or rejected, when one is held + // (novox/hq issues 441, 451): its check answered once, so a recheck is the step. Empty for a head with no + // delivery, and from a mesh-delivery that does not say it yet; the words are never read for it. + Held string `json:"held,omitempty"` + For string `json:"for"` + Bound string `json:"bound"` + H2 string `json:"h2"` + Says string `json:"says"` } // waitingCheck is one merge check a stalled line waits on, as mesh-delivery gives it: its context, its state — diff --git a/cmd/mesh-controller/plain_words.go b/cmd/mesh-controller/plain_words.go index 4ce02db1..35846e47 100644 --- a/cmd/mesh-controller/plain_words.go +++ b/cmd/mesh-controller/plain_words.go @@ -885,6 +885,11 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio // within its bound (novox/hq issue 438): mesh-delivery holds no delivery of it, so there is nothing to stop, release // or close, and no action is offered. The operator's acts are a new commit, which asks the check again, or a look // at the build queue, where a check that never ran may still wait. +// +// A head whose delivery is held in ready or rejected is said the same way (novox/hq issue 441), and mesh-delivery +// names that state as data (Held). Its check answered once, so the build queue is the wrong place to look: the step +// is a recheck of the delivery, named by its id, which asks the check again (novox/hq issue 451). It is advised, not +// offered as an action, since the controller is not granted the recheck. func unansweredWords(l stalledLine, o conditions.Observation) (headline, explanation, resolved, needs string, actions []conditions.Action) { repository, _, _ := strings.Cut(l.ID, "@") @@ -899,13 +904,23 @@ func unansweredWords(l stalledLine, o conditions.Observation) (headline, explana if d, err := time.ParseDuration(l.Bound); err == nil { limit = ", past its limit of " + humanDuration(d) } - which := "Its merge check" + check := "merge check" if said := uncheckedWaitWords(l); len(said) > 0 { - which = "Its merge check (" + strings.Join(said, ", ") + ")" + check = "merge check (" + strings.Join(said, ", ") + ")" } - if o.Resolver == conditions.ResolverOperator { - needs = "push a new commit to its branch, which asks the check again, or see whether its check still waits " + - "in the build queue: mesh-controller.queue." + which := "Its " + check + switch l.Held { + case "ready", "rejected": + which = "Its delivery is " + l.Held + ", but on the forge its " + check + if o.Resolver == conditions.ResolverOperator { + needs = "recheck its delivery, which asks its check again — mesh-delivery.recheck with id " + l.ID + + " and why — or push a new commit to its branch, " + FromMeshMCPServer + } + default: + if o.Resolver == conditions.ResolverOperator { + needs = "push a new commit to its branch, which asks the check again, or see whether its check still " + + "waits in the build queue: mesh-controller.queue." + } } return fmt.Sprintf("%s's merge check has not answered %s", pull, long), fmt.Sprintf("%s is open on a branch that requires the merge check. %s was asked and has not answered %s%s. "+ diff --git a/cmd/mesh-controller/plain_words_test.go b/cmd/mesh-controller/plain_words_test.go index cd025c19..f832f479 100644 --- a/cmd/mesh-controller/plain_words_test.go +++ b/cmd/mesh-controller/plain_words_test.go @@ -452,3 +452,49 @@ func TestAnUnansweredMergeChecksTimeIsSaidInLocalTime(t *testing.T) { t.Fatalf("a pending check without its time reads %q", o.Explanation) } } + +// **An unanswered merge check on a ready or rejected delivery is told to recheck it** (novox/hq issue 451): +// mesh-delivery says such a line too (issue 441) and names the delivery's state as data, `held`. That check already +// answered once, so the build queue is the wrong place to look; a recheck of the delivery, named by its id, asks the +// check again. A line with no delivery held keeps the advice of issue 438, and so does a line from a mesh-delivery +// that does not say `held` yet: the controller never guesses the state from the words. +func TestAnUnansweredMergeCheckOnAReadyOrRejectedDeliveryOffersARecheck(t *testing.T) { + line := func(held string) stalledLine { + var l stalledLine + raw := `{"id":"novox/mesh-controller@c11222026a3b","number":212,"state":"unanswered",` + + `"waiting":["mesh/merge-gate pending since 2026-10-11T00:11:39Z"],"for":"1h1m0s","bound":"1h0m0s",` + + `"h2":"none: its delivery is ` + held + `, a state the operator moves — the operator's",` + + `"says":"novox/mesh-controller#212 is open on main, which requires the merge check, and its delivery is ` + + held + ` here, but on the forge its check has not answered: mesh/merge-gate pending since ` + + `2026-10-11T00:11:39Z. The pull request waits on that status and nothing asks it again. A recheck of the ` + + `delivery asks its check again, as does a new commit on its branch"` + if held != "" { + raw += `,"held":"` + held + `"` + } + if err := json.Unmarshal([]byte(raw+`}`), &l); err != nil { + t.Fatal(err) + } + return l + } + for _, held := range []string{"ready", "rejected"} { + o := stalledObservations([]stalledLine{line(held)})[0] + t.Logf("%s: %s\nneeds: %s", held, o.Explanation, o.Needs) + if !strings.Contains(o.Needs, "mesh-delivery.recheck") || !strings.Contains(o.Needs, "novox/mesh-controller@c11222026a3b") { + t.Fatalf("a %s delivery's unanswered check is not offered its recheck: %q", held, o.Needs) + } + if strings.Contains(o.Needs, "build queue") || strings.Contains(o.Needs, "mesh-controller.queue") { + t.Fatalf("a %s delivery's unanswered check, which answered once, is sent to the build queue: %q", held, o.Needs) + } + if !strings.Contains(o.Explanation, "Its delivery is "+held) { + t.Fatalf("a %s delivery's explanation does not say its state: %q", held, o.Explanation) + } + if len(o.Actions) != 0 { + t.Fatalf("it offers an action the controller is not granted: %+v", o.Actions) + } + } + // No delivery held, or a mesh-delivery that does not say: the advice of issue 438, and no recheck. + o := stalledObservations([]stalledLine{line("")})[0] + if !strings.Contains(o.Needs, "in the build queue: mesh-controller.queue") || strings.Contains(o.Needs, "recheck") { + t.Fatalf("a line with no delivery held says to %q", o.Needs) + } +}