mesh/merge-gate pass: builds mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1791654663505629616 (novox/mesh-catalog at a14fa306 into main, do…
mesh/delivery-group group fix/313-retire-adopted-history closed: a member was closed before the group merged
234 lines
10 KiB
Go
234 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// History (novox/hq issue 313): merges long past, heard only from the forge's word when this owner first held
|
|
// the seat, reached the machines before it existed. They are retired — never held for a person, released or
|
|
// delivered — and what is owed for a pull request the forge no longer holds is given up, not tried for ever.
|
|
|
|
// aPastMerge is the forge's `pull.merged` replayed from the bus's history: merged days before it is heard.
|
|
func aPastMerge(repo string, number int, head, merge string, at time.Time) PullEvent {
|
|
p := merged(repo, number, head, merge)
|
|
p.MergedAt = at.UTC().Format(time.RFC3339)
|
|
return p
|
|
}
|
|
|
|
func TestAMergeLongPastHeardFromTheForgesWordIsRetiredAtAdoption(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullMerged(aPastMerge("novox/app", 274, head, merge, w.now.Add(-4*24*time.Hour)))
|
|
w.settleAll()
|
|
d := w.delivery(id)
|
|
if d.State != Retired || !strings.Contains(d.History, "history") {
|
|
t.Fatalf("a merge four days past heard from the forge's word is %s (%q)", d.State, d.History)
|
|
}
|
|
if got := statesOf([]State{d.Transitions[0].To, d.Transitions[1].To}); strings.Join(got, ",") != "proposed,retired" ||
|
|
d.Transitions[1].Event != EvHistory {
|
|
t.Fatalf("transitions %+v", d.Transitions)
|
|
}
|
|
// Nothing is said on its pull request — the forge may hold another under its number — and nothing stalls.
|
|
if w.forge.viewCalls != 0 || len(w.forge.statuses) != 0 {
|
|
t.Fatalf("history was shown on a pull request: %d view(s), statuses %v", w.forge.viewCalls, w.forge.statuses)
|
|
}
|
|
if len(d.Owed) != 0 {
|
|
t.Fatalf("still owed: %+v", d.Owed)
|
|
}
|
|
if s := w.h.Stalled(); len(s) != 0 {
|
|
t.Fatalf("history stalls: %+v", s)
|
|
}
|
|
// Its record is still kept, on the commit's note.
|
|
if len(w.forge.notesOn(head)) == 0 {
|
|
t.Fatal("no note of it on its commit")
|
|
}
|
|
|
|
// A merge heard from the forge's word within moments of happening is a live one: held for a person.
|
|
live := IDOf("novox/app", "bbbbbbbbbbbb2222")
|
|
w.h.PullMerged(aPastMerge("novox/app", 9, "bbbbbbbbbbbb2222", "cccccccccccc3333", w.now.Add(-time.Minute)))
|
|
if s := w.state(live); s != Held {
|
|
t.Fatalf("a live merge from the forge's word is %s", s)
|
|
}
|
|
// A merge long past whose walk the controller still holds open is delivered by it, never history.
|
|
walked := IDOf("novox/app", "dddddddddddd4444")
|
|
w.h.WalkMoved(aWalk("plan-9", "novox/app", "eeeeeeeeeeee5555", true))
|
|
w.h.PullMerged(aPastMerge("novox/app", 10, "dddddddddddd4444", "eeeeeeeeeeee5555", w.now.Add(-48*time.Hour)))
|
|
if s := w.state(walked); s == Retired {
|
|
t.Fatal("a merge with an open walk was retired as history")
|
|
}
|
|
// A merge the forge announces under a number an older delivery already merged with is not that delivery.
|
|
w.h.PullMerged(aPastMerge("novox/app", 274, "", "ffffffffffff6666", w.now.Add(-time.Minute)))
|
|
if d := w.delivery(id); d.MergedAs != merge {
|
|
t.Fatalf("a reused number merged the old delivery again as %s", d.MergedAs)
|
|
}
|
|
}
|
|
|
|
// oldHistory is a held delivery as the first adoption left it: made from the forge's word, held, owing a view
|
|
// on a pull request the forge no longer holds.
|
|
func oldHistory(id, repo string, number int, created, mergedAt time.Time) *Delivery {
|
|
commit := strings.SplitN(id, "@", 2)[1] + "0000"
|
|
return &Delivery{ID: id, Repository: repo, Commit: commit, Number: number, Base: "main", MergedAs: commit,
|
|
MergedAt: mergedAt, Created: created, Since: created, State: Held,
|
|
HeldWhy: "merged without a passing check: only a person decides that it goes on",
|
|
Transitions: []Transition{
|
|
{At: created, To: Proposed, Event: EvAnnounced, Why: repo + "#274 merged; its head was not heard by this " +
|
|
"owner, so it is made from the forge's word"},
|
|
{At: created, From: Proposed, To: Held, Event: EvMergedUnchecked, Why: "merged without a passing check"}},
|
|
Owed: []Effect{{Kind: EffectView, Since: created, Tries: 11051,
|
|
Last: "Gitea API /repos/" + repo + "/issues/274/comments: 500 issue does not exist"}}}
|
|
}
|
|
|
|
func TestRetireHistoryRetiresTheAdoptedHistoryInBulkAndLeavesEveryOtherHeldOne(t *testing.T) {
|
|
store := newMemStore()
|
|
start := time.Date(2026, 10, 6, 23, 5, 27, 0, time.UTC)
|
|
for _, d := range []*Delivery{
|
|
oldHistory("novox/hq@018ee359ae52", "novox/hq", 274, start, start.Add(-4*24*time.Hour)),
|
|
oldHistory("novox/lab@a4f6bb8ce054", "novox/lab", 53, start, start.Add(-2*time.Hour)),
|
|
// Made from the forge's word, but heard a minute after its merge: a live merge, a person's.
|
|
oldHistory("novox/hq@7777777777aa", "novox/hq", 3, start, start.Add(-time.Minute)),
|
|
} {
|
|
if err := store.PutDelivery(d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
w := newWorldOver(t, store, newFakeController(), newFakeForge(), start.Add(36*time.Hour))
|
|
// And one heard live: rejected, then merged — a person's to decide.
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "fail"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
if len(w.h.Stalled()) != 3 { // the three adopted at the switch; the one held just now is within its bound
|
|
t.Fatalf("stalled before: %+v", w.h.Stalled())
|
|
}
|
|
|
|
dry, err := w.h.RetireHistory("", "", "history adopted at the switch", aPerson, true)
|
|
if err != nil || dry.Retired != 2 || dry.LeftHeld != 2 || len(dry.Left) != 2 {
|
|
t.Fatalf("a dry retire: %+v %v", dry, err)
|
|
}
|
|
if s := w.state("novox/hq@018ee359ae52"); s != Held {
|
|
t.Fatalf("a dry retire moved a delivery to %s", s)
|
|
}
|
|
if _, err := w.h.RetireHistory("", "", "", aPerson, false); err == nil {
|
|
t.Fatal("a retire with no why was taken")
|
|
}
|
|
|
|
got, err := w.h.RetireHistory("", "", "history adopted at the switch", aPerson, false)
|
|
if err != nil || got.Retired != 2 || got.ByRepository["novox/hq"] != 1 || got.ByRepository["novox/lab"] != 1 {
|
|
t.Fatalf("retire: %+v %v", got, err)
|
|
}
|
|
for _, id := range []string{"novox/hq@018ee359ae52", "novox/lab@a4f6bb8ce054"} {
|
|
d := w.delivery(id)
|
|
if d.State != Retired || d.Transitions[len(d.Transitions)-1].By != aPerson {
|
|
t.Fatalf("%s is %s", id, d.State)
|
|
}
|
|
for _, e := range d.Owed {
|
|
if e.Kind == EffectView || e.Kind == EffectStatus {
|
|
t.Fatalf("%s still owes its pull request: %+v", id, e)
|
|
}
|
|
}
|
|
}
|
|
for _, id := range []string{"novox/hq@7777777777aa", IDOf("novox/app", head)} {
|
|
if s := w.state(id); s != Held {
|
|
t.Fatalf("%s, not history, is %s", id, s)
|
|
}
|
|
}
|
|
// Nothing was released, delivered or let go.
|
|
if len(w.ctl.delivers) != 0 || len(w.ctl.stops) != 0 {
|
|
t.Fatalf("a retire asked the controller: deliver %v stop %v", w.ctl.delivers, w.ctl.stops)
|
|
}
|
|
if s := w.h.Stalled(); len(s) != 1 || s[0].ID != "novox/hq@7777777777aa" {
|
|
t.Fatalf("stalled after: %+v", s)
|
|
}
|
|
// Retired is final: a second retire takes nothing, and nothing leaves it.
|
|
again, _ := w.h.RetireHistory("", "", "again", aPerson, false)
|
|
if again.Retired != 0 {
|
|
t.Fatalf("retired twice: %+v", again)
|
|
}
|
|
if _, err := w.h.Release("novox/hq@018ee359ae52", "go on", aPerson); err == nil {
|
|
t.Fatal("a retired delivery was released")
|
|
}
|
|
}
|
|
|
|
func TestRetireHistoryIsRefusedForARealHeldDelivery(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Checked(verdict("novox/app", 7, head, "fail"))
|
|
w.h.PullMerged(merged("novox/app", 7, head, merge))
|
|
w.later(48 * time.Hour)
|
|
_, err := w.h.RetireHistory(id, "", "looks old", aPerson, false)
|
|
var refused ErrRefused
|
|
if !errors.As(err, &refused) || !strings.Contains(err.Error(), "heard its head") {
|
|
t.Fatalf("a held delivery whose head was heard was retired: %v", err)
|
|
}
|
|
if s := w.state(id); s != Held {
|
|
t.Fatalf("a refused retire left it %s", s)
|
|
}
|
|
// A delivery that is not held is refused too, by the table.
|
|
other := IDOf("novox/app", "bbbbbbbbbbbb2222")
|
|
w.h.PullUpdated(pr("novox/app", 8, "bbbbbbbbbbbb2222", "feat/y", "src/b.go"))
|
|
if _, err := w.h.RetireHistory(other, "", "old", aPerson, false); err == nil || w.state(other) != Proposed {
|
|
t.Fatalf("a proposed delivery was retired: %v", err)
|
|
}
|
|
if _, err := w.h.RetireHistory("novox/app@nothing", "", "old", aPerson, false); err == nil {
|
|
t.Fatal("retired a delivery that does not exist")
|
|
}
|
|
}
|
|
|
|
func TestOwedWorkTheForgeSaysCanNeverBeDoneIsDropped(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.forge.viewErr = errors.New(`Gitea API /repos/novox/app/issues/7/comments: 500 {"message":"issue does not ` +
|
|
`exist [id: 0, repo_id: 53, index: 7]"}`)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.h.Flush()
|
|
if w.forge.viewCalls != 1 {
|
|
t.Fatalf("the view was asked %d time(s)", w.forge.viewCalls)
|
|
}
|
|
if d := w.delivery(id); len(d.Owed) != 0 {
|
|
t.Fatalf("an answer that its pull request does not exist left owed %+v", d.Owed)
|
|
}
|
|
w.settleAll()
|
|
if w.forge.viewCalls != 1 {
|
|
t.Fatalf("a dropped view was asked again: %d", w.forge.viewCalls)
|
|
}
|
|
if st := w.h.Status("listening"); !strings.Contains(strings.Join(st["refused"].([]string), "\n"), "given up") {
|
|
t.Fatalf("its drop was not said: %v", st["refused"])
|
|
}
|
|
|
|
// Any other failure is tried again later each time, and given up after retryLimit tries.
|
|
w.forge.viewErr = errors.New("the forge is away")
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
before := w.forge.viewCalls
|
|
w.h.Flush()
|
|
w.h.Flush()
|
|
w.h.Flush()
|
|
if w.forge.viewCalls != before+1 {
|
|
t.Fatalf("a failed view was tried %d time(s) at once", w.forge.viewCalls-before)
|
|
}
|
|
w.later(retryFirst + time.Second)
|
|
w.h.Flush()
|
|
if w.forge.viewCalls != before+2 {
|
|
t.Fatalf("a failed view was not tried again after its wait: %d", w.forge.viewCalls-before)
|
|
}
|
|
if retryAfter(1) != retryFirst || retryAfter(2) != 2*retryFirst || retryAfter(100) != retryCap {
|
|
t.Fatalf("the waits: %s %s %s", retryAfter(1), retryAfter(2), retryAfter(100))
|
|
}
|
|
for range retryLimit {
|
|
w.later(retryCap)
|
|
w.h.Flush()
|
|
}
|
|
for _, e := range w.delivery(id).Owed {
|
|
if e.Kind == EffectView {
|
|
t.Fatalf("a view failing %d times is still owed: %+v", e.Tries, e)
|
|
}
|
|
}
|
|
if !doesNotExist(errors.New("Gitea API /repos/a/b/issues/1/comments: 404 Not Found")) ||
|
|
doesNotExist(errors.New("tool gitea.gitea_delivery_view does not exist on this machine")) ||
|
|
doesNotExist(errors.New("the forge is away")) {
|
|
t.Fatal("doesNotExist reads the forge's answer wrong")
|
|
}
|
|
}
|