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

347 lines
15 KiB
Go

package main
import (
"errors"
"strings"
"testing"
"time"
)
const time30s = 31 * time.Second
// Issue 1275 (the 2026-10-19 18:05 group): a group is merged whole or not at all, in its order, and a group that is held or
// half merged is said loudly. Issue 1276/AC1: a refusal naming the member's own repository check as missing asks that check.
const (
gate, repoCheck = "mesh/merge-gate", "mesh/repo-check"
)
// statusOf sets a member head's two merge checks on the forge, and makes both required.
func setChecks(w *world, repository, sha, gateState, repoState string) {
w.forge.required = []string{gate, repoCheck}
for ctx, state := range map[string]string{gate: gateState, repoCheck: repoState} {
key := repository + "@" + sha + " " + ctx
if state == "" {
delete(w.forge.statuses, key) // no status at all: the forge says it is missing
continue
}
w.forge.statuses[key] = state + " said → x"
}
}
func allGreen(w *world) {
setChecks(w, gCat, gHk, "success", "success")
setChecks(w, gCtl, gHc, "success", "success")
}
func refusedWords(repository string) error {
return errors.New("the forge refused gitea_merge_pull_request: " + repository + " is not merged: main requires " +
"mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing). A pull request merges once its checks succeed")
}
func urgentLines(w *world) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.Severity == "urgent" {
out = append(out, l)
}
}
return out
}
// 1275/AC2: an approved group whose member has a required status that is not a success at its head when the merge starts
// is merged not at all, though the first member in order is fine; the group says which member and which status.
func TestAnApprovedGroupWithAMemberWhoseRequiredStatusIsNotASuccessMergesNothing(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing after the ask
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged with a required status missing: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || !strings.Contains(lines[0].Says, "feat/g") || !strings.Contains(lines[0].Says, IDOf(gCtl, gHc)) ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("the held group is not said as one urgent line naming group, member, status and need: %+v", lines)
}
// The status turns success: the whole group merges.
setChecks(w, gCtl, gHc, "success", "success")
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.later(time30s)
w.settleAll()
if len(w.forge.merges) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("merges %v, lines %+v; wanted both merged and no line", w.forge.merges, urgentLines(w))
}
}
// 1275/AC3: the merge tool refuses the first member in order; the second is not merged, and the group is said urgent,
// naming the group, the refused member and the forge's words.
func TestARefusedFirstMemberStopsTheGroupAndIsSaidUrgent(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || !strings.Contains(w.forge.merges[0], "mesh-catalog#2") {
t.Fatalf("wanted only the first member asked: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCat, gHk) || !strings.Contains(lines[0].Says, "feat/g") ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("not said as one urgent line: %+v", lines)
}
}
// 1275/AC4: a group half merged (the second member refused after the first merged) stays raised, pass after pass, until
// the second is merged; then it goes.
func TestAHalfMergedGroupStaysRaisedUntilEveryMemberIsMerged(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCtl: refusedWords("novox/mesh-controller#1")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
for range 3 {
w.later(time30s)
w.settleAll()
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCtl, gHc) || !strings.Contains(lines[0].Says, "feat/g") {
t.Fatalf("the half merged group is not raised: %+v", lines)
}
}
// A half merged group with no refusal on record (the pass died between members) is raised too.
d := w.delivery(IDOf(gCtl, gHc))
d.Ready.RefusedSince = nil
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "half merged") {
t.Fatalf("a half merged group without a refusal is not raised: %+v", lines)
}
w.forge.refuseRepo = nil
setChecks(w, gCtl, gHc, "success", "success")
w.h.PullMerged(merged(gCtl, 1, gHc, gT1))
w.settleAll()
if lines := urgentLines(w); len(lines) != 0 {
t.Fatalf("the group is whole and still raised: %+v", lines)
}
}
// 1275/AC5: a member's required status turning not-success while the ask waits withdraws the ask at the router; the
// answer to the withdrawn ask merges nothing; the group is asked again once every member may merge.
func TestAStatusThatTurnsWhileTheAskWaitsWithdrawsItAndItsAnswerMergesNothing(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
ask := w.op.open()[0]
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "pending"})
setChecks(w, gCat, gHk, "success", "pending")
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != ask {
t.Fatalf("the ask was not withdrawn at the router: %v", w.op.cancelled)
}
w.op.answer(ask, OptionApprove) // an answer that arrives late
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("the answer to a withdrawn ask merged: %v", w.forge.merges)
}
if w.op.count() != 1 {
t.Fatalf("asked again while a status was still pending: %d", w.op.count())
}
setChecks(w, gCat, gHk, "success", "success")
w.later(time30s)
w.settleAll()
if w.op.count() != 2 {
t.Fatalf("not asked again once every member may merge: %d", w.op.count())
}
}
// 1275/AC3 (review round 1): a member that is both refused and held for a required status is said by one stalled line,
// not two with the same id.
func TestAMemberBothRefusedAndHeldForAStatusIsOneStalledLine(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
setChecks(w, gCat, gHk, "success", "") // now its status is missing too; the next pass is held
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "success"})
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if g := w.h.groups["feat/g"]; g.Hold == nil {
t.Fatalf("the group was not held for the missing status")
}
n := 0
for _, l := range w.h.Stalled() {
if l.ID == IDOf(gCat, gHk) {
n++
}
}
if n != 1 {
t.Fatalf("%d stalled lines for one member: %+v", n, w.h.Stalled())
}
}
// 1275/AC6, as novox/hq ADR 0505 §4 changed it: a member refused because its verdict is out of date (the trunk moved; its
// fresh check runs) is no stalled line while the gate answers it, and urgent once ten minutes passed with the group not
// merged; the pass stops at the refusal all the same.
func TestAnOutOfDateRefusalIsNoLineWhileAnsweredAndUrgentAfterTenMinutes(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the pass did not stop at the refusal: %v", w.forge.merges)
}
sev := func() []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
if got := sev(); len(got) != 0 {
t.Fatalf("an out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(9 * time.Minute)
if got := sev(); len(got) != 0 {
t.Fatalf("at nine minutes it is %v; wanted still no line", got)
}
w.later(2 * time.Minute)
if got := sev(); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6: every other refusal (here a missing status) is urgent at once.
func TestAMissingStatusRefusalIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if lines := w.h.Stalled(); len(lines) != 1 || lines[0].Severity != "urgent" {
t.Fatalf("a missing status refusal is not urgent at once: %+v", lines)
}
}
func severities(w *world) []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
// 1275/AC6 (b), as ADR 0505 §4 changed it: the same out of date refusal on a LATER member, after the earlier one merged,
// is no line at once and urgent after ten minutes: the half merged group does not say it at once.
func TestAnOutOfDateRefusalOnALaterMemberIsNoLineThenUrgent(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
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 got := severities(w); len(got) != 0 {
t.Fatalf("a later member's out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(11 * time.Minute)
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6 (c): words naming an out of date check and also a missing status are urgent at once.
func TestARefusalNamingOutOfDateAndAMissingStatusIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: novox/mesh-catalog#2 is not merged: its verdict was " +
"judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. The check of head cccc merged into trunk bbbb was " +
"asked. main requires mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing)")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("a refusal with two causes is %v; wanted urgent at once", got)
}
}
// 1275/AC6 (d): words mesh-delivery cannot classify are urgent at once.
func TestARefusalInUnknownWordsIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: something nobody foresaw")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("an unclassified refusal is %v; wanted urgent at once", got)
}
}
// The merge tool's refusals, copied from gitea merge.ts with their line, `${...}` filled in.
var mergeToolRefusals = []struct {
line string
words string
outOfDated bool
}{
{"merge.ts:121", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"Its check is already asked and not yet answered (building), so it is not asked again; took 3s. Ask again at cccccccccccc once " +
"both statuses pass: this tool never merges by itself (novox/hq ADR 0323). A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:147", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"The check of head cccccccccccc merged into trunk bbbbbbbbbbbb was asked; took 3s. Ask again at cccccccccccc once both " +
"statuses pass: this tool never merges by itself (novox/hq ADR 0323). In a busy repository another merge may move the trunk " +
"again first; this is then refused and asked once more. A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:144", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb, " +
"and its check could not be asked: the forge is away. Nothing was merged", false},
{"merge.ts:156", "novox/app#7 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", false},
{"merge.ts:165", "novox/app#7 is not merged: its verdict read siblings, and which ones is not held here, so the group cannot " +
"be weighed. Ask its check again at cccccccccccc. Nothing was merged", false},
{"merge.ts:172", "novox/app#7 is not merged: its verdict was judged beside 1 sibling(s), and a sibling is not ready. A member " +
"judged beside its siblings merges once its group passed at the heads that verdict read (novox/hq ADR 0382, 0421). " +
"Nothing was merged", false},
}
// 1275/AC6 (e), as ADR 0505 §4 changed it: only the merge tool's two out of date refusals (:121, :147) are answered by
// the gate and said as no line at once; the one saying its check could not be asked (:144) and every other is urgent at
// once.
func TestOnlyTheMergeToolsTwoOutOfDateRefusalsWaitWhileAnswered(t *testing.T) {
for _, c := range mergeToolRefusals {
if got := outOfDateRefusal(c.words); got != c.outOfDated {
t.Errorf("%s: outOfDateRefusal = %v, wanted %v", c.line, got, c.outOfDated)
}
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: " + c.words)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
got := severities(w)
if c.outOfDated && len(got) != 0 {
t.Errorf("%s: said %v at once, wanted no line while it is answered", c.line, got)
}
if !c.outOfDated && (len(got) != 1 || got[0] != "urgent") {
t.Errorf("%s: severity %v at once, wanted urgent", c.line, got)
}
}
}
// 1275/AC6 (f): the severity never steps down: a missing status refusal, then an out of date one, stays urgent.
func TestASeverityOnceUrgentNeverStepsDownToAWarning(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.forge.refuseRepo = map[string]error{gCat: errAskedAgain}
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if d := w.delivery(IDOf(gCat, gHk)); !outOfDateRefusal(d.Ready.Answer) {
t.Fatalf("the second refusal was not the out of date one: %q", d.Ready.Answer)
}
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("severity %v after an out of date refusal that followed an urgent one; wanted urgent", got)
}
}