Merge remote-tracking branch 'origin/main' into l3-mc-main

This commit is contained in:
2026-10-29 02:15:33 +01:00
17 changed files with 77 additions and 102 deletions
@@ -91,9 +91,11 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
// nothing new.
var cores func() float64
var hours func() (*buildstep.BuildWindow, string)
settings := ""
if capacity == nil {
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
settings = m.Settings
watch = buildstep.WatchConfigServed
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskGo], m.Say)
}
@@ -106,7 +108,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
}
}
return &buildstep.Puller{Source: source, Language: delivery.AskGo, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say, Has: m.Has,
Capacity: capacity, SettingsFile: settings, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say, Has: m.Has,
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
if err := m.Check(ctx); err != nil {
return nil, err
@@ -148,7 +148,17 @@ func (m *Module) Health(ctx context.Context) health.Answer {
if err := m.room(); err != nil {
return health.Unhealthy(err.Error())
}
return health.Healthy(fmt.Sprintf("%s installed, the version the module builds with (recipe %s)", installed, m.Recipe.Version()))
note := ""
if m.Held != nil {
why, fault := m.Held.CapacityNote()
if fault {
return health.Unhealthy(why)
}
if why != "" {
note = "; " + why // a capacity set to 0 is a choice, said so a starved builder is seen
}
}
return health.Healthy(fmt.Sprintf("%s installed, the version the module builds with (recipe %s)", installed, m.Recipe.Version()) + note)
}
// Long is a tool that runs a long step (ADR 0469 §2): it answers at once with the step's id, and the step ends on
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
@@ -47,9 +47,11 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
// nothing new.
var cores func() float64
var hours func() (*buildstep.BuildWindow, string)
settings := ""
if capacity == nil {
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
settings = m.Settings
watch = buildstep.WatchConfigServed
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskImage], m.Say)
}
@@ -59,7 +61,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
}
return &buildstep.Puller{Source: source, Language: delivery.AskImage, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Eligible: m.Eligible, Logf: m.Say,
Capacity: capacity, SettingsFile: settings, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Eligible: m.Eligible, Logf: m.Say,
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
if err := m.Check(ctx); err != nil {
return nil, err
@@ -187,7 +187,17 @@ func (m *Module) Health(ctx context.Context) health.Answer {
// Not eligible: the module runs, it takes no image ask here now, and says why; the capacity record says it too.
return health.Healthy("not eligible to build images, so it takes no image ask: " + err.Error())
}
return health.Healthy(fmt.Sprintf("buildah %s installed; %s holds %d subordinate uids and %d gids; a user namespace can be made; %s opens", l.Buildah, l.Account, l.UIDs, l.GIDs, m.tunPath()))
note := ""
if m.Held != nil {
why, fault := m.Held.CapacityNote()
if fault {
return health.Unhealthy(why)
}
if why != "" {
note = "; " + why // a capacity set to 0 is a choice, said so a starved builder is seen
}
}
return health.Healthy(fmt.Sprintf("buildah %s installed; %s holds %d subordinate uids and %d gids; a user namespace can be made; %s opens", l.Buildah, l.Account, l.UIDs, l.GIDs, m.tunPath()) + note)
}
// Status is what the module reports of itself.
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildimage
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -55,9 +55,11 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
// nothing new.
var cores func() float64
var hours func() (*buildstep.BuildWindow, string)
settings := ""
if capacity == nil {
hours = buildstep.NodeBuildHours(m.Settings, m.Node, m.Say)
capacity = buildstep.NodeCapacity(m.Settings, m.Node, m.Say)
settings = m.Settings
watch = buildstep.WatchConfigServed
cores = buildstep.NodeCoresPerStep(m.Settings, buildstep.DefaultCoresPerStep[delivery.AskNode], m.Say)
}
@@ -67,7 +69,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
load = func(key string) ([]byte, error) { return m.ReadPauses.Get(key) }
}
return &buildstep.Puller{Source: source, Language: delivery.AskNode, Node: m.Node, Put: m.Steps, PausePut: m.Pauses, Load: load,
Capacity: capacity, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say,
Capacity: capacity, SettingsFile: settings, WatchConfig: watch, CoresPerStep: cores, BuildHours: hours, Logf: m.Say,
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
if err := m.Prepare(ctx); err != nil {
return nil, err
@@ -159,7 +159,17 @@ func (m *Module) Health(ctx context.Context) health.Answer {
if err := m.room(); err != nil {
return health.Unhealthy(err.Error())
}
return health.Healthy(fmt.Sprintf("node %s installed, the version the module builds with (recipe %s)", installed, m.recipe().Version()))
note := ""
if m.Held != nil {
why, fault := m.Held.CapacityNote()
if fault {
return health.Unhealthy(why)
}
if why != "" {
note = "; " + why // a capacity set to 0 is a choice, said so a starved builder is seen
}
}
return health.Healthy(fmt.Sprintf("node %s installed, the version the module builds with (recipe %s)", installed, m.recipe().Version()) + note)
}
// Long is a tool that runs a long step (ADR 0469 §2): it answers at once with the step's id, and the step ends on
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5 h1:L7EJbCPZfJRCYHyco5Ck3ui+pWc//sp+h5nnPqSarng=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028190821-9e5666b4d6f5/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
@@ -157,8 +157,8 @@ func checkClause(c checkSaid, sep string) string {
return ": " + c.Words
}
// approveCaveat is what an Approve does beyond the merge while the check has not passed: it merges before the check ends,
// or although it failed (the check gates no merge, check-gates-merge false). Empty once it passed.
// approveCaveat is what an Approve does beyond the merge while the check has not passed: it merges although it failed (the check gates
// no merge, check-gates-merge false). Empty once it passed; an ask is made only with a verdict.
func approveCaveat(c checkSaid) string {
switch c.Key {
case checkSaidPass:
@@ -166,7 +166,7 @@ func approveCaveat(c checkSaid) string {
case checkSaidFail:
return " although its check failed"
}
return " now, before its check ends"
return ""
}
// composeMergeAsk is the ask for a delivery, or for a group's members together.
@@ -411,9 +411,6 @@ func (h *Holder) approval(d *Delivery, now time.Time) (string, func()) {
}
}
check := h.checkAt(members)
if !h.checkGates() {
check = unansweredWords(check) // asked with no verdict only past its bound (verdictWaits)
}
ask := composeMergeAsk(sdk.AskingID(id, asking), d, group, members, heads, check, now)
taken := []*Delivery{}
var reads []memberRead
@@ -79,10 +79,7 @@ type Holder struct {
unchecked map[string]unchecked
// checker are the checker's verdicts by the hash of the heads they judged, as its verdicts bucket holds them
// (onestatus.go, issue 1302); runningSince when this owner first saw each running (ADR 0548 §2, turns.go).
checker map[string]checkerVerdict
// verdictGone is when the checker's verdict at a heads hash was taken away (its key deleted): the Approve's wait for a
// verdict counts its bound from then, as from a check that starts again (verdictWaits).
verdictGone map[string]time.Time
checker map[string]checkerVerdict
runningSince map[string]time.Time
// unmergeableSince is when the forge first said a group's member does not merge now, by group and member (issue 1275,
// group_merge.go): within forgeMergeGrace that is the forge still working out mergeability, asked again shortly.
@@ -146,7 +143,7 @@ func (h *Holder) init() {
h.lostRead = map[string]time.Time{}
h.unchecked = map[string]unchecked{}
h.checker = map[string]checkerVerdict{}
h.runningSince, h.turnsHeld, h.verdictGone = map[string]time.Time{}, map[string]sdk.Turn{}, map[string]time.Time{}
h.runningSince, h.turnsHeld = map[string]time.Time{}, map[string]sdk.Turn{}
h.reach = map[string]sdk.Reach{}
h.started = h.Now()
}
@@ -82,13 +82,9 @@ func (h *Holder) CheckerVerdictChanged(key string, value []byte) {
if h.runningSince == nil {
h.runningSince = map[string]time.Time{}
}
if h.verdictGone == nil {
h.verdictGone = map[string]time.Time{}
}
if len(value) == 0 || string(value) == "null" {
delete(h.checker, hash)
delete(h.runningSince, hash)
h.verdictGone[hash] = h.Now()
h.Poke()
return
}
@@ -104,7 +100,6 @@ func (h *Holder) CheckerVerdictChanged(key string, value []byte) {
h.runningSince[v.Heads] = h.Now() // a check starts: a merge it holds says since when (ADR 0548 §2)
} else if v.State != checkerRunning {
delete(h.runningSince, v.Heads)
delete(h.verdictGone, v.Heads)
}
h.checker[v.Heads] = v
h.Poke()
@@ -453,67 +453,20 @@ func (h *Holder) ungatedWaits(d *Delivery) string {
return ""
}
// askCheckBound is how long an Approve waits for the check's verdict at its heads while the check gates no merge: past it,
// the operator is asked anyway, and the ask says the check has not answered. The checker's own step bound is 30 minutes
// and a check runs several steps; an hour covers a whole check.
const askCheckBound = time.Hour
// waitsCheckBeforeAsking is how the wait for the check's verdict begins, as `show` and the delivery's wait say it.
const waitsCheckBeforeAsking = "waits its check before asking the operator's Approve: "
// verdictWaits is why an Approve is not asked yet while the check gates no merge: the check at the heads the ask would cover
// has no verdict (the operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request").
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and so is a check that has not
// answered within askCheckBound of the heads becoming ready, which the ask says. For a group the check is the one of all its
// heads together. Under the lock.
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and nothing else: no timer stands
// in for the verdict, because the checker turns a check it lost into a failure it says aloud (issue 1733, the operator,
// 2026-10-28: "one hour is absurd"). For a group the check is the one of all its heads together. Under the lock.
func (h *Holder) verdictWaits(d *Delivery) string {
members := h.askMembers(d)
c := h.checkAt(members)
c := h.checkAt(h.askMembers(d))
if c.Key == checkSaidPass || c.Key == checkSaidFail {
return ""
}
until := h.waitSince(d, members).Add(askCheckBound)
if !h.Now().Before(until) {
return ""
}
h.lookAt(until, "an Approve that waits its check, asked anyway at its bound")
return waitsCheckBeforeAsking + c.Words + "; asked anyway at " + clockSaid(until) + " if it has not answered"
}
// waitSince is when the wait for a verdict began, the moment the bound of verdictWaits counts from: the latest of the
// heads becoming ready (a member with no ready run yet, such as one re-pushed or settled ready by an event this look has
// not reached, became ready now), the check at those heads starting again, and its verdict being taken away. Under the lock.
func (h *Holder) waitSince(d *Delivery, members []*Delivery) time.Time {
now := h.Now()
since := time.Time{}
later := func(t time.Time) {
if t.After(since) {
since = t
}
}
for _, m := range append([]*Delivery{d}, members...) {
if m.Ready == nil {
return now
}
later(m.Ready.Seen)
}
if hash, err := sdk.HeadsHash(headsOf(members)); err == nil {
later(h.runningSince[hash])
later(h.verdictGone[hash])
}
if since.IsZero() || since.After(now) {
return now
}
return since
}
// unansweredWords is the check's words in an ask made past askCheckBound with no verdict: it says the check has not answered.
func unansweredWords(c checkSaid) checkSaid {
if c.Key == checkSaidPass || c.Key == checkSaidFail {
return c
}
c.Words += ", and has given none in " + strconv.Itoa(int(askCheckBound.Minutes())) + " minutes, so the operator is asked anyway"
return c
return waitsCheckBeforeAsking + c.Words
}
// waitsForReview is how a hold for a review begins, as the delivery's state and its mesh/delivery status say it.
@@ -71,8 +71,8 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
// 1733: the Approve's ask says the check's state at the heads it names, read from the checker's verdict there: "passed
// its check" only when it passed. While the check gates no merge, the ask waits for the check's verdict (the operator,
// 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request"): a pass or a failure is asked,
// the failure in its words; with no verdict nothing is asked and the delivery says it waits its check, until the bound,
// past which it is asked anyway, saying so, and asked again in new words once the check passes. The verdict is the
// the failure in its words; with no verdict nothing is asked and the delivery says it waits its check, until the verdict
// comes, however long (issue 1733: no timer stands in for it). The verdict is the
// checker's verdicts bucket value as captured on the bus (mesh-delivery-checker, `<group>.<heads hash>`).
for _, row := range []struct {
name, state, failed, then string
@@ -84,21 +84,19 @@ func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
{name: "gated, passed", gated: true, state: "pass", asks: 1,
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; asked anyway at 15:00 CEST if"}},
{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 past its bound", state: "running", after: askCheckBound, asks: 1,
want: []string{"its check has no verdict yet: it is running", "has given none in 60 minutes, so the operator is asked anyway",
"Approve merges it into main at " + readyHead[:12] + " now, before its check ends"}, not: []string{"passed"}},
{name: "ungated, past its bound, then passed", state: "running", after: askCheckBound, then: "pass", asks: 2,
{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,
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) past the bound
// withdraws the ask, and the bound counts again from that moment: nothing is asked at once as unanswered.
{name: "ungated, passed, running again past its bound", state: "pass", after: askCheckBound, then: "running", asks: 1, open: -1,
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet: it is running (since 15:00 CEST); asked anyway at 16:00 CEST"}},
{name: "ungated, passed, its verdict gone past its bound", state: "pass", after: askCheckBound, then: "gone", asks: 1, open: -1,
want: []string{waitsCheckBeforeAsking + "its check has no verdict yet; asked anyway at 16:00 CEST"}},
// 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,
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
} {
@@ -369,8 +367,8 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
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)
}
// A member re-pushed past the bound after its sibling became ready: the bound counts from the new head, so nothing is
// asked as unanswered at once (review round 1).
// A member re-pushed hours after its sibling became ready: with no verdict at the new heads nothing is asked, however
// long the sibling has waited.
r := newWorld(t)
ungated(r)
r.op.auto = false
@@ -378,17 +376,16 @@ func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
r.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
r.settleAll()
r.later(askCheckBound + time.Minute)
r.later(5 * time.Hour)
r.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead2, "feat/x", "internal/a.go"))
// Made ready by a handler, before the look gives it a run: no verdict and no status at the new heads, so only the rule
// that a member with no run counts as ready now keeps the older sibling's time from asking it as unanswered.
// Made ready by a handler, before the look gives it a run: no verdict and no status at the new heads.
r.h.mu.Lock()
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 past the bound: asked %d times, wait %+v; wanted none asked and the wait said", r.op.count(), d.Ready)
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)
}
u.op.answer(u.op.open()[0], OptionApprove)