422 lines
19 KiB
Go
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)
|
|
}
|
|
}
|