Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/group_test.go
T

444 lines
19 KiB
Go

package main
import (
"encoding/json"
"errors"
"sort"
"strings"
"testing"
"time"
)
// **A group is merged in one pass on its one Approve, and delivered by one plan** (novox/hq ADR 0418, ADR 0419, ADR
// 0421, issues 978 and 1047): the members are not ordered, none waits for another's merge or delivery, and a trunk
// move does not ask the operator a second time.
const (
gCtl, gCat = "novox/mesh-controller", "novox/mesh-catalog"
gHc, gHk = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
gTrunk0, gT1 = "a2c2c35000000000000000000000000000000000", "d28ca95000000000000000000000000000000000"
)
// approvedGroup is a group of two pull requests on one branch, checks green, composed green and the operator's Approve
// asked and not yet answered (w.op.auto is off).
func approvedGroup(t *testing.T) *world {
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()
g := w.h.groups["feat/g"]
if g == nil || g.Check == nil {
t.Fatalf("no composed check was asked: %+v", g)
}
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed",
Members: []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{{Owner: "novox", Repo: "mesh-controller", Commit: gHc, Trunk: gTrunk0[:12]},
{Owner: "novox", Repo: "mesh-catalog", Commit: gHk, Trunk: gTrunk0[:12]}}})
w.settleAll()
if w.op.count() != 1 || len(w.op.open()) != 1 {
t.Fatalf("the green group is asked once: %d asks, open %v", w.op.count(), w.op.open())
}
return w
}
// Issue 1047 (the rule the lead settled, ADR 0421 §3): an approved group whose trunk moved through a merge that is not
// its member's has each out-of-date member's check asked at once, then the composed check, and merges on the same
// Approve — with no second one asked.
func TestAnApprovedGroupWhoseTrunkMovedIsCheckedAgainAndMergedOnTheSameApprove(t *testing.T) {
w := approvedGroup(t)
asked := len(w.ctl.checks)
w.h.PullMerged(merged(gCtl, 5, "5555555555555555", gT1))
w.settleAll()
// The member whose trunk moved has its own check asked, in the same look; the other member's trunk did not move.
if got := w.ctl.checks[asked:]; len(got) != 1 || got[0] != ":"+IDOf(gCtl, gHc) {
t.Fatalf("the trunk's move asked %v; wanted the moved member's own check once", got)
}
if w.ctl.baseSHAs[len(w.ctl.baseSHAs)-1] != gT1[:12] && w.ctl.baseSHAs[len(w.ctl.baseSHAs)-1] != gT1 {
t.Fatalf("its check was not asked against the trunk's head: %v", w.ctl.baseSHAs)
}
// The Approve is given while the checks run: nothing merges on the verdict out of date.
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged while the group's checks were out of date: %v", w.forge.merges)
}
// The member is judged again: the composed check is asked on the new verdicts.
w.later(time.Minute)
w.h.Checked(judged(gCtl, 1, gHc, "pass", gT1))
w.settleAll()
g := w.h.groups["feat/g"]
last := w.ctl.checks[len(w.ctl.checks)-1]
if last != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc) {
t.Fatalf("the composed check was not asked again: %v", w.ctl.checks)
}
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed again"})
w.settleAll()
sort.Strings(w.forge.merges)
if len(w.forge.merges) != 2 || w.forge.merges[0] != "novox/mesh-catalog#2@"+gHk || w.forge.merges[1] != "novox/mesh-controller#1@"+gHc {
t.Fatalf("both members were not merged on the new composed verdict: %v", w.forge.merges)
}
if w.op.count() != 1 {
t.Fatalf("a second Approve was asked: %d", w.op.count())
}
}
// ADR 0418 §4, issue 1275/AC3: a member the forge refuses stops the pass (the members after it are not asked), and is
// merged again on its fresh check, the others after it in the same pass.
func TestAMemberTheForgeRefusesStopsThePassAndIsMergedAgainOnItsFreshCheck(t *testing.T) {
w := approvedGroup(t)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the pass stops at the first refusal: %v", w.forge.merges)
}
w.forge.refuse = nil
w.later(2 * time.Minute)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("a refused member is asked again with no event, on a timer or at each look: %v", w.forge.merges)
}
w.h.Checked(CheckedEvent{Group: "feat/g", ID: w.h.groups["feat/g"].Check.Asked, Verdict: "pass", Summary: "composed again"})
w.settleAll()
if len(w.forge.merges) != 3 {
t.Fatalf("a refused member is asked again on the next composed verdict, then the next: %v", w.forge.merges)
}
if d := w.delivery(IDOf(gCat, gHk)); d.Ready.Merged == nil {
t.Fatalf("not merged: %+v", d.Ready)
}
}
// Issue 978 (the pin): a feature's single pull request in one repository is a lone delivery, asked alone.
func TestAFeaturesSinglePullRequestIsALoneDeliveryAskedAlone(t *testing.T) {
w := newWorld(t)
w.op.auto = false
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.settleAll()
d := w.delivery(IDOf("novox/app", readyHead))
if d.Group != "" || len(w.h.groups) != 0 {
t.Fatalf("a single pull request made a group: %q %v", d.Group, w.h.groups)
}
if w.op.count() != 1 {
t.Fatalf("a lone delivery is asked once, alone: %d", w.op.count())
}
}
// A group answer says its members' gate is the composed check's (agreed with the controller, issue 977).
func TestTheGroupsAnswerSaysItsGateIsTheComposedChecks(t *testing.T) {
w := approvedGroup(t)
lines := w.h.Groups(false)
if len(lines) != 1 || !lines[0].ComposedGate {
t.Fatalf("groups: %+v", lines)
}
raw, _ := json.Marshal(lines[0])
var got map[string]any
if err := json.Unmarshal(raw, &got); err != nil || got["composed_gate"] != true {
t.Fatalf("the group line says %s", raw)
}
if _, ordered := got["order"]; ordered {
t.Fatalf("a group line still says an order: %s", raw)
}
}
// ADR 0403 §1: a member whose own gate the group's composed check judges instead (the controller says its gate layer
// "composed") counts as passed here once its repository's check did not fail, so the group's composed check is asked.
func TestAMemberWhoseGateIsTheComposedChecksCountsAsPassed(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"))
for _, c := range []struct {
repo string
n int
head string
}{{gCtl, 1, gHc}, {gCat, 2, gHk}} {
v := judged(c.repo, c.n, c.head, "pass", gTrunk0)
v.Gate = &struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
}{Verdict: GateComposed, Summary: "judged by its group's composed check"}
v.RepoCheck = &struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}{Verdict: "pass", Summary: "the repository's own check passed"}
w.h.Checked(v)
}
w.settleAll()
if len(w.ctl.checks) != 1 || w.ctl.checks[0] != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc) {
t.Fatalf("the composed check was not asked for members judged by it: %v", w.ctl.checks)
}
// A member whose repository check failed is not passed, whatever the composed check will say.
w2 := newWorld(t)
w2.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
v := judged(gCtl, 1, gHc, "pass", gTrunk0)
v.Gate = &struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
}{Verdict: GateComposed}
v.RepoCheck = &struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}{Verdict: "fail", Summary: "a test failed"}
w2.h.Checked(v)
if w2.state(IDOf(gCtl, gHc)) == Ready {
t.Fatalf("a member whose repository check failed is ready")
}
}
// Issue 1049 (survey question 1, confirmed here): a composed check asked after one member has already merged is asked
// with ALL the members, the merged one at the head it merged, never with the unmerged one alone. The controller needs two
// or more members to compose (mesh-controller delivery.go askGroupCheck), and a merge of a contained head is a no-op
// there.
func TestAComposedCheckAskedAfterOneMemberMergedIsAskedWithAllMembers(t *testing.T) {
w := approvedGroup(t)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
w.settleAll()
if g := w.h.groups["feat/g"]; g == nil || !g.Closed {
t.Fatalf("the group did not close when a member merged: %+v", g)
}
// The unmerged member gets a new head and its own verdict: the group's composed check is due again.
const gHc2 = "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
asked := len(w.ctl.checks)
w.h.PullUpdated(pr(gCtl, 1, gHc2, "feat/g", "internal/a.go"))
w.h.Checked(judged(gCtl, 1, gHc2, "pass", gT1))
w.settleAll()
var composed []string
for _, c := range w.ctl.checks[asked:] {
if len(c) > 7 && c[:7] == "feat/g:" {
composed = append(composed, c)
}
}
if len(composed) != 1 || composed[0] != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc2) {
t.Fatalf("the composed check was asked with %v; wanted once, with both members (the merged one at its head)", composed)
}
}
// Issue 1082 (the 2026-10-17 23:48 group): a closed group's member whose merge the tool refused after its sibling merged
// asks the group's composed check again at once, and merges on its pass with the one Approve (ADR 0418 §4, ADR 0421).
func TestAClosedGroupsMemberWhoseMergeWasRefusedAsksTheComposedCheckAgainAtOnce(t *testing.T) {
w := approvedGroup(t)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
// The catalogue member merged, the controller's was refused.
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
asked := len(w.ctl.checks)
w.settleAll()
var composed []string
for _, c := range w.ctl.checks[asked:] {
if strings.HasPrefix(c, "feat/g:") {
composed = append(composed, c)
}
}
if len(composed) != 1 || composed[0] != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc) {
t.Fatalf("the refusal asked the composed check %v; wanted once, at once, with both members", composed)
}
// Not again while that check is on its way.
w.later(2 * time.Minute)
w.settleAll()
if got := w.ctl.checks[asked:]; len(got) != 1 {
t.Fatalf("asked again while the composed check was on its way: %v", got)
}
// It passes; the controller's member merges on the same Approve.
w.forge.refuseRepo = nil
g := w.h.groups["feat/g"]
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed again"})
w.settleAll()
if d := w.delivery(IDOf(gCtl, gHc)); d.Ready.Merged == nil {
t.Fatalf("the refused member was not merged on the new composed verdict: %+v", d.Ready)
}
if w.op.count() != 1 {
t.Fatalf("a second Approve was asked: %d", w.op.count())
}
}
// Issue 1082: a refusal loop is not silent. A merge refused past ten minutes in a closed group (ADR 0418 §3) is a stalled
// line naming the member and the merge tool's last answer, which the controller raises as a condition.
func TestARefusalLoopInAClosedGroupIsSaidAtOnce(t *testing.T) {
w := approvedGroup(t)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
w.settleAll()
// Each composed pass is followed by a refusal again: the loop.
for range 12 {
w.later(time.Minute)
g := w.h.groups["feat/g"]
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed again"})
w.settleAll()
}
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].ID != IDOf(gCtl, gHc) || lines[0].Bound != "0s" ||
!strings.Contains(lines[0].Says, errAskedAgain.Error()) || !strings.Contains(lines[0].Says, "group feat/g") {
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:])
}
// The composed check that was on its way when the trunk moved answers, judged against the old trunk (ADR 0403 §6): it is
// out of date as it lands, so the group does not become ready on it and the operator is not asked.
g := w.h.groups["feat/g"]
w.later(time.Minute)
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed on the old trunk",
Members: []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{{Owner: "novox", Repo: "mesh-controller", Commit: gHc, Trunk: gTrunk0[:12]},
{Owner: "novox", Repo: "mesh-catalog", Commit: gHk, Trunk: gTrunk0[:12]}}})
w.settleAll()
if w.op.count() != 0 {
t.Fatalf("the operator was asked on a composed pass judged against the old trunk: %d", w.op.count())
}
// The moved member's fresh verdict arrives: the composed check is asked again, and only its pass asks the operator.
w.later(time.Minute)
composedBefore := len(w.ctl.checks)
w.h.Checked(judged(gCat, 2, gHk, "pass", gT1))
w.settleAll()
last := w.ctl.checks[len(w.ctl.checks)-1]
if len(w.ctl.checks) == composedBefore || !strings.HasPrefix(last, "feat/g:") {
t.Fatalf("the composed check was not asked again on the fresh verdict: %v", w.ctl.checks[composedBefore:])
}
if w.op.count() != 0 {
t.Fatalf("the operator was asked before the new composed check passed: %d", w.op.count())
}
g = w.h.groups["feat/g"]
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed on the new trunk",
Members: []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{{Owner: "novox", Repo: "mesh-controller", Commit: gHc, Trunk: gTrunk0[:12]},
{Owner: "novox", Repo: "mesh-catalog", Commit: gHk, Trunk: gT1[:12]}}})
w.settleAll()
if w.op.count() != 1 {
t.Fatalf("the operator is asked once, on the new pass: %d", w.op.count())
}
}
// Issue 1047 (review round 2): the other order. The moved member's fresh verdict lands before the stale composed pass (one
// build against two): the pass is out of date as it lands, the composed check is asked again, and the operator is asked
// only after that check passes.
func TestAStaleComposedPassLandingAfterTheMovedMembersFreshVerdictIsAskedAgain(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()
g := w.h.groups["feat/g"]
w.later(time.Minute)
w.h.PullMerged(merged(gCat, 5, "5555555555555555", gT1))
w.settleAll()
w.later(time.Minute)
w.h.Checked(judged(gCat, 2, gHk, "pass", gT1)) // the fresh verdict first
w.settleAll()
w.later(time.Minute)
before := len(w.ctl.checks)
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed on the old trunk",
Members: []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{{Owner: "novox", Repo: "mesh-controller", Commit: gHc, Trunk: gTrunk0[:12]},
{Owner: "novox", Repo: "mesh-catalog", Commit: gHk, Trunk: gTrunk0[:12]}}})
w.settleAll()
if len(w.ctl.checks) == before || !strings.HasPrefix(w.ctl.checks[len(w.ctl.checks)-1], "feat/g:") {
t.Fatalf("the composed check was not asked again after the stale pass: %v", w.ctl.checks[before:])
}
if w.op.count() != 0 {
t.Fatalf("the operator was asked before the new composed check passed: %d", w.op.count())
}
}
// ownPending is the merge tool's refusal of a member whose own mesh/repo-check stands pending after the group's composed
// check set its gate (issue 1049: the forge's "not a success: mesh/repo-check (pending)").
var ownPending = errors.New("the forge refused gitea_merge_pull_request: novox/mesh-controller#1 is not merged: main requires " +
"mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (pending). A pull request merges once its checks succeed")
// ownChecks are the checks asked from index on that were a head's own (no group), not a group's composed check.
func ownChecks(w *world, from int) (own, composed []string) {
for _, c := range w.ctl.checks[from:] {
if strings.HasPrefix(c, ":") {
own = append(own, c)
} else {
composed = append(composed, c)
}
}
return
}
// Issue 1049 (the 2026-10-18 02:04 group): a closed group's member refused for a pending status of its OWN (the repository
// check a sibling's new head put back) asks its own check, once for the head, and not the composed check, which sets no
// repository check; a second pass at the same head asks nothing more.
func TestAClosedGroupsMemberRefusedForItsOwnPendingStatusAsksItsOwnCheckOncePerHead(t *testing.T) {
w := approvedGroup(t)
w.forge.refuseRepo = map[string]error{gCtl: ownPending}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
asked := len(w.ctl.checks)
w.settleAll()
own, composed := ownChecks(w, asked)
if len(composed) != 0 || len(own) != 1 || own[0] != ":"+IDOf(gCtl, gHc) {
t.Fatalf("own checks %v, composed checks %v; wanted the member's own check once and no composed check", own, composed)
}
// Later passes, the same head, the same refusal: nothing more is asked.
w.later(2 * time.Minute)
w.settleAll()
w.later(2 * time.Minute)
w.settleAll()
if got := len(w.ctl.checks) - asked; got != 1 {
t.Fatalf("asked %d checks at the same head, wanted the one: %v", got, w.ctl.checks[asked:])
}
}