The gate asks the Approve at once while the check gates no merge, never waiting for a verdict (issue 1966)
mesh/delivery merged: source.main moved to novox/mesh-catalog@21aa11aef1af
mesh/check checking 145cf7e33afa on 21aa11aef1af

With check-gates-merge false (the configuration seat's value) the gate still held the Approve until the check had
a verdict (verdictWaits), so a one-line recovery waited half an hour for a whole-suite check it does not gate. The
operator, 2026-11-07: 'we aren't running any checks anymore, we're delivering'; 'disable the check'. The Approve is
now asked once the delivery is reviewed and ready; a later verdict is still shown and asks the pending Approve again
in its words. Tests follow: asked at once with no verdict or a running check, a group too, a re-pushed member too.

Merged by the session as a recovery fix (instruction recovery-fixes-merge-and-tell), the operator told on Telegram.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-07 21:46:34 +01:00
parent 9a3356bd38
commit 145cf7e33a
2 changed files with 26 additions and 30 deletions
@@ -456,7 +456,8 @@ func (h *Holder) readyWaits(d *Delivery) string {
}
// ungatedWaits is readyWaits while mesh/check gates no merge (check-gates-merge false): a ready delivery waits only for its
// review at its head, and a group member for every member to be ready; no verdict, composed check or trunk is waited on.
// review at its head, and a group member for every member to be ready; no verdict, composed check or trunk is waited on,
// so the Approve is asked at once.
func (h *Holder) ungatedWaits(d *Delivery) string {
if d.State != Ready {
return "it is " + string(d.State) + ", not ready yet"
@@ -473,9 +474,9 @@ func (h *Holder) ungatedWaits(d *Delivery) string {
}
}
}
if h.covering(d) == nil {
return h.verdictWaits(d)
}
// No verdict is waited on (the operator, 2026-11-07: "we aren't running any checks anymore, we're delivering"): while
// the check gates no merge, the Approve is asked once the delivery is reviewed and ready; a verdict that comes later
// is shown on the pull request and the delivery, as before.
return ""
}
@@ -85,20 +85,14 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
{name: "gated, passed", gated: true, state: "pass", asks: 1, head: "Merge into main: app#7 Fix it",
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
not: []string{"before its check ends", "no verdict yet"}},
{name: "ungated, no verdict", asks: 0, want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
{name: "ungated, running", state: "running", asks: 0, want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running"}},
{name: "ungated, running for hours, still not asked", state: "running", after: 5 * time.Hour, asks: 0,
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running"}, not: []string{"anyway"}},
{name: "ungated, running, then passed", state: "running", after: 2 * time.Hour, then: "pass", asks: 1,
want: []string{"passed its check. Approve merges it into main at " + readyHead[:12] + ";"}, not: []string{"before its check ends", "anyway"}},
{name: "ungated, passed", state: "pass", asks: 1,
// The operator, 2026-11-07: "we aren't running any checks anymore, we're delivering": while the check gates no
// merge the Approve is asked at once, with no verdict or a running check, never waiting for one.
{name: "ungated, no verdict", asks: 1, not: []string{waitsCheckBeforeAsking}},
{name: "ungated, running", state: "running", asks: 1, not: []string{waitsCheckBeforeAsking}},
// Asked at once, then asked again in its words once the verdict comes (a later verdict asks a pending Approve again).
{name: "ungated, passed", state: "pass", asks: 2,
want: []string{"passed its check. Approve merges it into main at " + readyHead[:12] + ";"}, not: []string{"before its check ends"}},
// A pass that turns back into a check with no verdict (a check started again, its verdict taken away) withdraws the ask.
{name: "ungated, passed, running again", state: "pass", then: "running", asks: 1, open: -1,
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running"}, not: []string{"anyway"}},
{name: "ungated, passed, its verdict gone", state: "pass", then: "gone", asks: 1, open: -1,
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet"}, not: []string{"anyway"}},
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 1,
{name: "ungated, failed", state: "fail", failed: "TestThing: wanted 1, got 2", asks: 2,
head: "Check failed \u2014 merge into main: app#7 Fix it",
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
} {
@@ -175,8 +169,8 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
u.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
u.h.PullUpdated(pr("novox/app", 8, other, "feat/y", "src/b.go"))
u.settleAll()
if u.op.count() != 0 || u.state(IDOf("novox/app", readyHead)) != Ready {
t.Fatalf("ungated, two reviewed pull requests with no verdict: %d asks, %s; wanted ready and not asked", u.op.count(), u.state(IDOf("novox/app", readyHead)))
if u.op.count() == 0 || u.state(IDOf("novox/app", readyHead)) != Ready {
t.Fatalf("ungated, two reviewed pull requests with no verdict: %d asks, %s; wanted ready and asked at once", u.op.count(), u.state(IDOf("novox/app", readyHead)))
}
if gates, _ := u.h.Status("listening")["check-gates-merge"].(bool); gates {
t.Fatalf("delivery_status does not say the check gates no merge: %v", u.h.Status("listening"))
@@ -189,8 +183,8 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
checkerSays(u, "feat/y", "fail", "TestB", headOf("novox/app", 8, other))
u.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
u.settleAll()
if u.op.count() != 1 {
t.Fatalf("ungated, a failed verdict is asked once (the check gates no merge): %d asks", u.op.count())
if u.op.count() == 0 {
t.Fatalf("ungated, a failed verdict holds the ask (the check gates no merge): %d asks", u.op.count())
}
d = u.delivery(IDOf("novox/app", other))
if d.State != Ready || d.Check == nil || d.Check.Passes() {
@@ -359,15 +353,15 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
group := []mdc.Head{headOf("novox/mesh-controller", 1, ctlHead), headOf("novox/mesh-catalog", 2, catHead)}
checkerSays(u, "feat/x", "running", "", group...)
u.settleAll()
// The group's check is the one of its heads together: while it runs, nothing is asked and each member says it waits.
if d := u.delivery(IDOf("novox/mesh-catalog", catHead)); u.op.count() != 0 || d.Ready == nil ||
!strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking+"its check has no verdict yet: it is running") {
t.Fatalf("ungated, the group is asked %d times while its check runs; wanted none, and the wait said: %+v", u.op.count(), d.Ready)
// Issue 1966 (the operator, 2026-11-07): the group is asked at once, its check running or not; nothing waits on it.
if d := u.delivery(IDOf("novox/mesh-catalog", catHead)); u.op.count() != 1 || d.Ready == nil ||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
t.Fatalf("ungated, the group is asked %d times while its check runs; wanted once at once, and the wait said: %+v", u.op.count(), d.Ready)
}
checkerSays(u, "feat/x", "pass", "", group...)
u.settleAll()
if u.op.count() != 1 {
t.Fatalf("ungated, the group is asked %d times once its check passed; wanted once", u.op.count())
if u.op.count() != 2 {
t.Fatalf("ungated, the group is asked %d times once its check passed; wanted once at once and once again in its words", u.op.count())
}
if e := u.op.ask(u.op.open()[0]).Explanation; !strings.Contains(e, "2 pull requests, passed its check. Approve merges all of them into main in one pass, at the heads named;") {
t.Fatalf("ungated, the group's ask does not say its check passed:\n%s", e)
@@ -388,9 +382,10 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
r.h.ungate(r.h.deliveries[IDOf("novox/mesh-controller", ctlHead2)], r.h.Now())
r.h.mu.Unlock()
r.settleAll()
if d := r.delivery(IDOf("novox/mesh-catalog", catHead)); r.op.count() != 0 || d.Ready == nil ||
!strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
t.Fatalf("a member re-pushed hours later: asked %d times, wait %+v; wanted none asked and the wait said", r.op.count(), d.Ready)
// Issue 1966: no verdict is waited on, so the new heads are asked again at once, never held for a check.
if d := r.delivery(IDOf("novox/mesh-catalog", catHead)); r.op.count() == 0 || d.Ready == nil ||
strings.Contains(d.Ready.Waits, waitsCheckBeforeAsking) {
t.Fatalf("a member re-pushed hours later: asked %d times, wait %+v; wanted asked again at once", r.op.count(), d.Ready)
}
u.op.answer(u.op.open()[0], OptionApprove)