diff --git a/modules/mesh-delivery/cmd/mesh-delivery/group_test.go b/modules/mesh-delivery/cmd/mesh-delivery/group_test.go index faf97c2d..35a91e24 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/group_test.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/group_test.go @@ -285,3 +285,35 @@ func TestARefusalLoopInAClosedGroupIsSaidPastTenMinutes(t *testing.T) { t.Fatalf("the loop was not said, naming the member, its group and the tool's answer: %+v", lines) } } + +// Issue 1047 (the live case of 2026-10-17 23:34, ADR 0395 §1, design 47): a group member that is ready when a third party's +// merge moves its trunk, with no Approve asked yet (the composed check has not passed), has its own check asked at once, +// and the Approve is asked once the group's composed check has passed on it. +func TestATrunkMoveAsksTheCheckOfAGroupMemberWhoseApproveIsNotAskedYet(t *testing.T) { + w := newWorld(t) + w.op.auto = false + w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go")) + w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json")) + w.h.Checked(judged(gCtl, 1, gHc, "pass", gTrunk0)) + w.h.Checked(judged(gCat, 2, gHk, "pass", gTrunk0)) + w.settleAll() + if w.op.count() != 0 { + t.Fatalf("the composed check has not passed, yet the operator was asked: %d", w.op.count()) + } + asked := len(w.ctl.checks) + w.h.PullMerged(merged(gCat, 5, "5555555555555555", gT1)) + w.settleAll() + got := w.ctl.checks[asked:] + if len(got) != 1 || got[0] != ":"+IDOf(gCat, gHk) { + t.Fatalf("the trunk's move asked %v; wanted the moved member's own check once", got) + } + if last := w.ctl.baseSHAs[len(w.ctl.baseSHAs)-1]; last != gT1[:12] && last != gT1 { + t.Fatalf("its check was not asked against the trunk's head: %v", w.ctl.baseSHAs) + } + // Not asked again at the next look while it is on its way. + w.later(time.Second) + w.settleAll() + if len(w.ctl.checks[asked:]) != 1 { + t.Fatalf("asked twice: %v", w.ctl.checks[asked:]) + } +} diff --git a/modules/mesh-delivery/cmd/mesh-delivery/holder.go b/modules/mesh-delivery/cmd/mesh-delivery/holder.go index a91fbee6..4e18c0a2 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/holder.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/holder.go @@ -616,7 +616,9 @@ func (h *Holder) PullMerged(p PullEvent) { // trunkMoved takes a merge into a trunk as that trunk's move (novox/hq ADR 0323 §7): every other open delivery into // it keeps the trunk's new head, and a ready one whose verdict names the commit before goes back to proposed. A // group's composed check goes out of date when the merge is not one of its members and a member still open merges -// into that trunk (§6); the group then reads checking. Nothing is asked: a recheck or the merge tool judges again. +// into that trunk (§6); the group then reads checking. Nothing is asked here: at the same look askFresh asks the check of +// a group's member, or of a head with an Approve asked or given (issue 1047); a lone head's is judged again by a recheck +// or the merge tool (the rest of ADR 0395 §1 is issue 888). func (h *Holder) trunkMoved(merged *Delivery, base, commit string, now time.Time) { if base == "" || commit == "" { return diff --git a/modules/mesh-delivery/cmd/mesh-delivery/ready.go b/modules/mesh-delivery/cmd/mesh-delivery/ready.go index fb2de9f7..4c7449d3 100644 --- a/modules/mesh-delivery/cmd/mesh-delivery/ready.go +++ b/modules/mesh-delivery/cmd/mesh-delivery/ready.go @@ -245,7 +245,10 @@ func (h *Holder) askFresh(d *Delivery, now time.Time) func() { d.Number == 0 || d.TrunkNow == "" { return nil } - if !d.FreshWanted && h.covering(d) == nil { + // A group's member too (issue 1047, design 47: a move of the trunk asks every check again): the group's one Approve is + // asked after its composed check, so a member ready before that has no Approve to carry; its check is asked at once + // all the same, and the composed check follows on the new verdict. The rest of ADR 0395 §1 stays with issue 888. + if !d.FreshWanted && h.covering(d) == nil && h.groups[d.Group] == nil { return nil } if tried, was := h.askTried[d.ID]; was && now.Sub(tried) < time.Minute {