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

422 lines
19 KiB
Go

package main
import (
"errors"
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
)
// **A pull request whose checks passed and whose merge the operator approved is merged by this owner** (novox/hq issues
// 722 and 1022): before, a reviewed pull request with green checks waited until a session called the merge tool, and
// again after every check the tool asked because the trunk had moved.
const (
readyHead = "1111111111111111111111111111111111111111"
pushed = "2222222222222222222222222222222222222222"
)
// openPull is an open pull request into main as gitea_open_pulls reads it.
func openPull(repo string, number int, head string) OpenPull {
owner, name, _ := strings.Cut(repo, "/")
return OpenPull{Owner: owner, Repo: name, Number: number, Base: "main", HeadSHA: head}
}
func judgedAt(repo string, number int, sha, gate, trunk string) CheckedEvent {
c := verdict(repo, number, sha, gate)
c.Trunk = trunk
return c
}
func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := IDOf("novox/app", readyHead)
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()
if len(w.forge.merges) != 0 {
t.Fatalf("a pull request nobody approved was merged: %v", w.forge.merges)
}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
t.Fatalf("an approved pull request was merged %v; wanted once, at the approved head", w.forge.merges)
}
d := w.delivery(id)
if d.Ready == nil || d.Ready.Merged == nil || d.Ready.Tries != 1 {
t.Fatalf("what was done with the Approve is not kept: %+v", d.Ready)
}
// Its merge's transition names the Approve as its warrant.
w.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
w.settleAll()
d = w.delivery(id)
var last Transition
for _, tr := range d.Transitions {
if tr.Event == EvMerged {
last = tr
}
}
if !strings.Contains(last.By, "the operator's Approve at "+readyHead[:12]) {
t.Fatalf("the merge's transition does not name the Approve: %+v", last)
}
if len(w.forge.merges) != 1 {
t.Fatalf("merged again: %v", w.forge.merges)
}
// 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 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
gated bool
after time.Duration
asks, open int
want, not []string
}{
{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"}, 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,
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,
want: []string{"its check failed: TestThing: wanted 1, got 2", "although its check failed"}, not: []string{"passed", "anyway"}},
} {
t.Run(row.name, func(t *testing.T) {
v := newWorld(t)
if !row.gated {
ungated(v)
}
v.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Title = "Fix it"
v.h.PullUpdated(p)
head := headOf("novox/app", 7, readyHead)
if row.gated {
v.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
}
v.settleAll()
if !row.gated && row.state != "" {
checkerSaysWith(v, "g", row.state, row.failed, nil, head)
v.settleAll()
}
if row.after > 0 {
v.later(row.after)
v.settleAll()
}
if row.then != "" {
checkerSays(v, "g", row.then, row.failed, head)
v.settleAll()
}
open := v.op.open()
wantOpen := min(row.asks, 1)
if row.open < 0 {
wantOpen = 0
}
if v.op.count() != row.asks || len(open) != wantOpen {
t.Fatalf("open asks %v, asked %d times; wanted asked %d times, %d open", open, v.op.count(), row.asks, wantOpen)
}
if wantOpen == 0 {
d := v.delivery(IDOf("novox/app", readyHead))
for _, w := range row.want {
if d.Ready == nil || !strings.Contains(d.Ready.Waits, w) {
t.Fatalf("the delivery's wait does not say %q: %+v", w, d.Ready)
}
}
return
}
e := v.op.ask(open[0]).Explanation
for _, w := range row.want {
if !strings.Contains(e, w) {
t.Fatalf("the ask does not say %q:\n%s", w, e)
}
}
for _, w := range row.not {
if strings.Contains(e, w) {
t.Fatalf("the ask says %q:\n%s", w, e)
}
}
if err := asks.Glance(e); err != nil {
t.Fatal(err)
}
})
}
// check-gates-merge false (the operator, 2026-10-25 22:56): a reviewed pull request is asked with no verdict, another
// merge moving its trunk puts it out of nothing, a failed verdict is shown on it and holds nothing, and the gate's
// status and show say the mode.
u := newWorld(t)
ungated(u)
u.op.auto = false
const other = "3333333333333333333333333333333333333333"
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 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"))
}
shown, _ := u.h.Show(IDOf("novox/app", other))
if gates, _ := shown.(map[string]any)["check-gates-merge"].(bool); gates {
t.Fatalf("show does not say the check gates no merge: %v", shown)
}
u.h.Checked(judgedAt("novox/app", 8, other, "fail", "t1t1t1t1t1t1"))
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())
}
d = u.delivery(IDOf("novox/app", other))
if d.State != Ready || d.Check == nil || d.Check.Passes() {
t.Fatalf("ungated, after a failed verdict and a trunk's move: %s, verdict %+v", d.State, d.Check)
}
for _, a := range u.op.open() {
if strings.Contains(a, "feat/y") {
u.op.answer(a, OptionApprove)
}
}
u.settleAll()
if len(u.forge.merges) != 1 || u.forge.merges[0] != "novox/app#8@"+other {
t.Fatalf("ungated, the approved head is merged %v; wanted once, with no verdict waited on", u.forge.merges)
}
}
// 1144/AC3: a refused merge is not asked again by any timer; only an event (here, mergeAgainNow called directly, as a
// verdict, an Approve or a composed check does) asks it again.
func TestAMergeRefusedIsSaidAtOnceAndAskedAgainOnlyOnAnEvent(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check is pending")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "refused it since") {
t.Fatalf("a merge refused is not said at once: %+v", lines)
}
w.later(time.Hour)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("asked again with no event: %v", w.forge.merges)
}
again := w.delivery(IDOf("novox/app", readyHead))
w.h.mu.Lock()
w.h.mergeAgainNow(again) // what a verdict, an Approve or a composed check does
w.h.mu.Unlock()
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("not asked again on an event: %v", w.forge.merges)
}
}
// 1144/AC1: a refused merge, approved and ready, is asked again at once when the forge's forge.status says one of
// the merge checks (mesh/merge-gate, mesh/repo-check) is success for the commit the merge was refused at — the
// delivery's approved head — and merges once the tool accepts (novox/hq mesh-catalog#366: the verdict reached this
// owner before the forge wrote the statuses, the merge asked then was refused on a pending check, and nothing asked
// it again when the statuses turned success).
func TestAMergeRefusedIsAskedAgainWhenItsCheckTurnsSuccessOnTheForge(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("not asked once, refused: %v", w.forge.merges)
}
w.forge.refuse = nil
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "app", SHA: readyHead, Context: "mesh/repo-check", State: "success"})
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("not asked again when its check turned success: %v", w.forge.merges)
}
if w.delivery(IDOf("novox/app", readyHead)).Ready.Merged == nil {
t.Fatalf("it did not merge once the tool accepted")
}
}
// 1144/AC2: a status of another context, of another commit, or a state other than success changes nothing for a
// refused merge: it is not asked again.
func TestAMergeRefusedIsNotAskedAgainOnAStatusThatDoesNotMatch(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("not asked once, refused: %v", w.forge.merges)
}
w.forge.refuse = nil
for _, e := range []ForgeStatusEvent{
{Owner: "novox", Repo: "app", SHA: readyHead, Context: "some/other-check", State: "success"}, // another context
{Owner: "novox", Repo: "app", SHA: pushed, Context: "mesh/repo-check", State: "success"}, // another commit
{Owner: "novox", Repo: "app", SHA: readyHead, Context: "mesh/repo-check", State: "pending"}, // another state
} {
w.h.CommitStatus(e)
w.settleAll()
}
if len(w.forge.merges) != 1 {
t.Fatalf("asked again on a status that should change nothing: %v", w.forge.merges)
}
if w.delivery(IDOf("novox/app", readyHead)).Ready.Merged != nil {
t.Fatalf("merged on a status that should change nothing")
}
}
// 1144/AC4: the stalled line for a refused approved head names the events that ask the merge again, and no longer
// says "each minute" (removed by issue 1115; false since then — there is no timer).
func TestTheStalledLineForARefusedApprovedHeadNamesTheEventsThatAskTheMergeAgain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
lines := w.h.Stalled()
if len(lines) != 1 {
t.Fatalf("wanted one stalled line, got %+v", lines)
}
h2 := lines[0].H2
if strings.Contains(h2, "each minute") {
t.Fatalf("the stalled line still says 'each minute', false since issue 1115: %q", h2)
}
for _, want := range []string{"verdict", "Approve", "composed check", "status"} {
if !strings.Contains(h2, want) {
t.Fatalf("the stalled line does not name %q as asking the merge again: %q", want, h2)
}
}
}
func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("composed, the group is asked once and nothing merges before the Approve: asks %d, merges %v", w.op.count(), w.forge.merges)
}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("both members are merged in the same pass on the one Approve: %v", w.forge.merges)
}
sort.Strings(w.forge.merges)
if w.forge.merges[0] != "novox/mesh-catalog#2@"+catHead || w.forge.merges[1] != "novox/mesh-controller#1@"+ctlHead {
t.Fatalf("merged %v, wanted both, each at its approved head", w.forge.merges)
}
for _, got := range w.forge.approvedHeads {
if len(got) != 2 || got[0] != "novox/mesh-catalog@"+catHead || got[1] != "novox/mesh-controller@"+ctlHead {
t.Fatalf("a member's merge was handed the approved heads %v", got)
}
}
if w.op.count() != 1 {
t.Fatalf("a second Approve was asked: %d", w.op.count())
}
// check-gates-merge false: the group is asked once with no verdict and no composed check, and merges whole in one pass.
u := newWorld(t)
ungated(u)
u.op.auto = false
u.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
u.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
u.settleAll()
group := []sdk.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)
}
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 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 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
const ctlHead2 = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
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(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.
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 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)
u.settleAll()
if len(u.forge.merges) != 2 {
t.Fatalf("ungated, both members merge in one pass: %v", u.forge.merges)
}
}
// **A merge into another branch than main is its author's** (novox/hq ADR 0398 §4, issue 948): a green pull request
// whose base is not main is never asked or merged by this owner; it says so once on the pull request.
func TestAGreenPullRequestIntoAnotherBranchIsNotAskedOrMergedAndSaidOnce(t *testing.T) {
w := newWorld(t)
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Base = "release/x"
w.h.PullUpdated(p)
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
open := openPull("novox/app", 7, readyHead)
open.Base = "release/x"
w.forge.open = []OpenPull{open}
for range 3 {
w.h.SeeOpenPulls()
w.settleAll()
w.later(2 * time.Minute)
}
if len(w.forge.merges) != 0 || w.op.count() != 0 {
t.Fatalf("a pull request into release/x was merged %v or asked %d", w.forge.merges, w.op.count())
}
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "another branch") ||
!strings.Contains(w.forge.comments[0], "0398") {
t.Fatalf("not said exactly once on the pull request: %v", w.forge.comments)
}
}