Say an unanswered merge check's time in the operator's zone, from the checks given as data (novox/hq issue 443)
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check fail: its merge-check.sh failed: FAIL github.com/novox/mesh-controller/cmd/mesh-controller 268.072s
mesh/delivery-group group fix/443-a-stalled-lines-times-are-local rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-11 03:10:30 +02:00
parent 47c7877e8d
commit 926fde2fda
3 changed files with 105 additions and 6 deletions
+19 -4
View File
@@ -40,12 +40,27 @@ type stalledLine struct {
// Waiting are the merge checks a line of the state `unanswered` waits on, as mesh-delivery says each: // Waiting are the merge checks a line of the state `unanswered` waits on, as mesh-delivery says each:
// "mesh/merge-gate pending since <UTC time>", "mesh/repo-check never set" (novox/hq issue 438). // "mesh/merge-gate pending since <UTC time>", "mesh/repo-check never set" (novox/hq issue 438).
Waiting []string `json:"waiting,omitempty"` Waiting []string `json:"waiting,omitempty"`
For string `json:"for"` // Checks are the same merge checks as data, which the controller words in the operator's own time (novox/hq
Bound string `json:"bound"` // issue 443): a time inside a finished sentence cannot be said again in another zone. A mesh-delivery from before
H2 string `json:"h2"` // says none, and Waiting is said as it reads.
Says string `json:"says"` Checks []waitingCheck `json:"checks,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 —
// "pending", or "never-set" — and, for a pending one, since when in RFC 3339, empty when the forge did not say.
type waitingCheck struct {
Context string `json:"context"`
State string `json:"state"`
Since string `json:"since,omitempty"`
}
// wordsNow is the time the words are said at, which says whether a time needs its day; a seam a test replaces.
var wordsNow = time.Now
// operatorsOnly is whether the table leaves H2 nothing to do for the line: the state is the operator's. // operatorsOnly is whether the table leaves H2 nothing to do for the line: the state is the operator's.
func (l stalledLine) operatorsOnly() bool { return l.H2 == "" || strings.HasPrefix(l.H2, "none") } func (l stalledLine) operatorsOnly() bool { return l.H2 == "" || strings.HasPrefix(l.H2, "none") }
+38 -2
View File
@@ -879,8 +879,8 @@ func unansweredWords(l stalledLine, o conditions.Observation) (headline, explana
limit = ", past its limit of " + humanDuration(d) limit = ", past its limit of " + humanDuration(d)
} }
which := "Its merge check" which := "Its merge check"
if len(l.Waiting) > 0 { if said := uncheckedWaitWords(l); len(said) > 0 {
which = "Its merge check (" + strings.Join(l.Waiting, ", ") + ")" which = "Its merge check (" + strings.Join(said, ", ") + ")"
} }
if o.Resolver == conditions.ResolverOperator { 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 " + needs = "push a new commit to its branch, which asks the check again, or see whether its check still waits " +
@@ -892,6 +892,42 @@ func unansweredWords(l stalledLine, o conditions.Observation) (headline, explana
fmt.Sprintf("%s has an answer from its merge check, or is closed", pull), needs, nil fmt.Sprintf("%s has an answer from its merge check, or is closed", pull), needs, nil
} }
// uncheckedWaitWords are the merge checks an unanswered line waits on, as the operator reads them: from the checks
// given as data, each time in the controller's local zone, as every other time in a message — "mesh/merge-gate
// pending since 02:11" — never the UTC time inside mesh-delivery's finished words, which cannot be said again in
// another zone (novox/hq issue 443). A line from a mesh-delivery that gives no such data is said by its words.
func uncheckedWaitWords(l stalledLine) []string {
if len(l.Checks) == 0 {
return l.Waiting
}
out := make([]string, 0, len(l.Checks))
for _, c := range l.Checks {
switch c.State {
case "never-set":
out = append(out, c.Context+" never set")
case "pending":
out = append(out, c.Context+" pending"+sinceWords(c.Since))
default:
out = append(out, c.Context+" not answered")
}
}
return out
}
// sinceWords is " since 02:11" in the controller's local zone, with its day when that is not today ("since 23:05 on
// 9 Oct"), so the time stays absolute; a time not given, or not readable, is said so and never made up.
func sinceWords(since string) string {
at, err := time.Parse(time.RFC3339, since)
if err != nil {
return ", since a time the forge did not say"
}
local, today := at.Local(), wordsNow().Local()
if local.YearDay() == today.YearDay() && local.Year() == today.Year() {
return " since " + local.Format("15:04")
}
return " since " + local.Format("15:04 on 2 Jan")
}
// causeWords is a hand-act's cause as a person says it. // causeWords is a hand-act's cause as a person says it.
func causeWords(cause string) string { func causeWords(cause string) string {
return strings.NewReplacer(".", " ", "_", " ").Replace(cause) return strings.NewReplacer(".", " ", "_", " ").Replace(cause)
+48
View File
@@ -402,3 +402,51 @@ func TestAnUnansweredMergeCheckSaysWhichCheckAndWhatToDo(t *testing.T) {
t.Fatalf("a line naming no checks reads %q / %q, actions %+v", o.Headline, o.Explanation, o.Actions) t.Fatalf("a line naming no checks reads %q / %q, actions %+v", o.Headline, o.Explanation, o.Actions)
} }
} }
// **An unanswered merge check's time reaches the operator in their own time, never as raw UTC** (novox/hq issue
// 443): mesh-delivery says each check waited on as data — its context, its state and since when, in RFC 3339 — beside
// the finished words it gave before, and the controller words it in its local zone, as every other time in a message.
// The line is the one mesh-delivery says, as in the report of issue 443, with the checks it now carries.
func TestAnUnansweredMergeChecksTimeIsSaidInLocalTime(t *testing.T) {
was, wasNow := time.Local, wordsNow
time.Local = time.FixedZone("CEST", 2*60*60)
wordsNow = func() time.Time { return time.Date(2026, 10, 11, 1, 12, 39, 0, time.UTC) }
t.Cleanup(func() { time.Local, wordsNow = was, wasNow })
var l stalledLine
if err := json.Unmarshal([]byte(`{"id":"novox/mesh-controller@c11222026a3b","number":212,"state":"unanswered",`+
`"waiting":["mesh/merge-gate pending since 2026-10-11T00:11:39Z","mesh/repo-check never set"],`+
`"checks":[{"context":"mesh/merge-gate","state":"pending","since":"2026-10-11T00:11:39Z"},`+
`{"context":"mesh/repo-check","state":"never-set"}],"for":"1h1m0s",`+
`"bound":"1h0m0s","h2":"none: no delivery of it is held here, so there is none to close — the operator's",`+
`"says":"novox/mesh-controller#212 is open on main, which requires the merge check, and its head's check `+
`started and never answered: mesh/merge-gate pending since 2026-10-11T00:11:39Z, mesh/repo-check never set. `+
`No delivery of it is held here, so nothing asks it again. A new commit on its branch announces it and asks `+
`its check"}`), &l); err != nil {
t.Fatal(err)
}
o := stalledObservations([]stalledLine{l})[0]
t.Logf("explanation: %s", o.Explanation)
for _, raw := range []string{"2026-10-11T00:11:39Z", "00:11", "UTC"} {
if strings.Contains(o.Explanation, raw) || strings.Contains(o.Headline, raw) {
t.Fatalf("the operator reads %q: %q / %q", raw, o.Headline, o.Explanation)
}
}
for _, want := range []string{"mesh/merge-gate pending since 02:11", "mesh/repo-check never set"} {
if !strings.Contains(o.Explanation, want) {
t.Fatalf("its explanation does not say %q: %q", want, o.Explanation)
}
}
// A check pending since another day than today says which day, so the time stays absolute.
l.Checks[0].Since = "2026-10-09T21:05:00Z"
if o = stalledObservations([]stalledLine{l})[0]; !strings.Contains(o.Explanation, "mesh/merge-gate pending since 23:05 on 9 Oct") {
t.Fatalf("a check pending since an earlier day reads %q", o.Explanation)
}
// A pending check whose time the forge did not say is said so, with no time made up.
l.Checks[0].Since = ""
if o = stalledObservations([]stalledLine{l})[0]; !strings.Contains(o.Explanation, "mesh/merge-gate pending, since a time the forge did not say") {
t.Fatalf("a pending check without its time reads %q", o.Explanation)
}
}