From dc86203d29edffd9521c2f9cd59ab42324cadf34 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 20:46:46 +0200 Subject: [PATCH] Say a refusal for an out of date verdict a warning for ten minutes, urgent after (issue 1275, operator's amendment, AC6) --- .../cmd/mesh-delivery/issue1275_test.go | 43 +++++++++++++++++++ .../mesh-delivery/cmd/mesh-delivery/ready.go | 32 +++++++++++--- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go b/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go index c7b08cb0..71faee0a 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/issue1275_test.go @@ -248,3 +248,46 @@ func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) { t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled()) } } + +// 1275/AC6: a member refused because its verdict is out of date (the trunk moved; its fresh check runs) is a warning at +// once, and urgent only when the group is not merged within ten minutes; the pass stops at the refusal all the same. +func TestAnOutOfDateRefusalIsAWarningAtOnceAndUrgentAfterTenMinutes(t *testing.T) { + w := approvedGroup(t) + allGreen(w) + w.forge.refuse = errAskedAgain + w.op.answer(w.op.open()[0], OptionApprove) + w.settleAll() + if len(w.forge.merges) != 1 { + t.Fatalf("the pass did not stop at the refusal: %v", w.forge.merges) + } + sev := func() []string { + var out []string + for _, l := range w.h.Stalled() { + out = append(out, l.Severity) + } + return out + } + if got := sev(); len(got) != 1 || got[0] != "warning" { + t.Fatalf("an out of date refusal is %v at once; wanted one warning", got) + } + w.later(9 * time.Minute) + if got := sev(); len(got) != 1 || got[0] != "warning" { + t.Fatalf("at nine minutes it is %v; wanted still a warning", got) + } + w.later(2 * time.Minute) + if got := sev(); len(got) != 1 || got[0] != "urgent" { + t.Fatalf("after ten minutes it is %v; wanted urgent", got) + } +} + +// 1275/AC6: every other refusal (here a missing status) is urgent at once. +func TestAMissingStatusRefusalIsUrgentAtOnce(t *testing.T) { + w := approvedGroup(t) + allGreen(w) + w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")} + w.op.answer(w.op.open()[0], OptionApprove) + w.settleAll() + if lines := w.h.Stalled(); len(lines) != 1 || lines[0].Severity != "urgent" { + t.Fatalf("a missing status refusal is not urgent at once: %+v", lines) + } +} diff --git a/modules/mesh-delivery/cmd/mesh-delivery/ready.go b/modules/mesh-delivery/cmd/mesh-delivery/ready.go index 88fa960c..03b938c5 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/ready.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/ready.go @@ -432,7 +432,7 @@ func (h *Holder) readyLines(now time.Time) []StalledLine { out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(*r.RefusedSince).Round(time.Second).String(), Bound: groupRefusedFor.String(), H2: "none: the group's composed check is asked again after each refusal, and the merge each minute — the operator's", - Severity: "urgent", + Severity: refusalSeverity(r, now), Says: fmt.Sprintf("group %s is half merged: %s#%d is approved at %s and the merge tool has refused it since %s: %s. "+ "Needed: the refusal's cause is met (the merge is asked again at each verdict, status or Approve), or the operator decides.", d.Group, d.Repository, d.Number, shortOf(r.Head, 12), r.RefusedSince.UTC().Format(time.RFC3339), r.Answer)}) @@ -443,7 +443,7 @@ func (h *Holder) readyLines(now time.Time) []StalledLine { "status turning success — the operator's", Says: fmt.Sprintf("%s%s#%d is approved at %s, and the merge tool has refused it since %s: %s%s", heldInGroup(d), d.Repository, d.Number, shortOf(r.Head, 12), r.RefusedSince.UTC().Format(time.RFC3339), r.Answer, neededOf(d)), - Severity: severityOf(d)}) + Severity: severityOf(d, now)}) } } return out @@ -482,10 +482,30 @@ func neededOf(d *Delivery) string { return ". Needed: the refusal's cause is met (the merge is asked again at each verdict, status or Approve), or the operator decides." } -// severityOf is "urgent" for a member of a group (a refused merge is the group held), empty for a lone delivery. -func severityOf(d *Delivery) string { - if d.Group == "" { - return "" +// outOfDateFor is how long a refusal for an out of date verdict is a warning before it is urgent (issue 1275, AC6). +const outOfDateFor = 10 * time.Minute + +// outOfDateRefusal is whether the merge tool refused because the member's verdict is judged against an earlier trunk: the +// forge's "its verdict was judged against trunk , and main is now . The check of head merged into trunk was +// asked" (gitea merge.ts), whose fresh check is already running. +func outOfDateRefusal(answer string) bool { + return strings.Contains(answer, "its verdict ") && strings.Contains(answer, " is now ") || + strings.Contains(answer, "merged into trunk") && strings.Contains(answer, "was asked") +} + +// refusalSeverity is "warning" while a member's refusal for an out of date verdict is younger than ten minutes, as its +// fresh check runs, and "urgent" for it after that and for every other refusal at once. +func refusalSeverity(r *ReadyRun, now time.Time) string { + if r.RefusedSince != nil && outOfDateRefusal(r.Answer) && now.Sub(*r.RefusedSince) < outOfDateFor { + return "warning" } return "urgent" } + +// severityOf is the severity of a refused member of a group, empty for a lone delivery. +func severityOf(d *Delivery, now time.Time) string { + if d.Group == "" { + return "" + } + return refusalSeverity(d.Ready, now) +}