263 lines
10 KiB
Go
263 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// **A verdict holds for the trunk commit it judged** (novox/hq ADR 0323, issue 341). The check judges the head
|
|
// merged into a named trunk commit; when another merge moves that trunk, a ready delivery goes back to proposed,
|
|
// a rejected one stays rejected, and nothing is asked. A verdict for an old trunk is kept and shown and does not
|
|
// count, so the table reaches a fixed point. A group's composed check goes out of date on a merge that is not one of
|
|
// its members, and runs again for new heads instead of holding the group.
|
|
|
|
const (
|
|
trunk0 = "a2c2c35000000000000000000000000000000000"
|
|
merge1 = "d28ca95000000000000000000000000000000000"
|
|
)
|
|
|
|
// judged is a verdict of a head merged into a trunk commit.
|
|
func judged(repo string, number int, sha, gate, trunk string) CheckedEvent {
|
|
c := verdict(repo, number, sha, gate)
|
|
c.Trunk = shortOf(trunk, 12)
|
|
c.ID = "check-of-" + sha + "-at-" + shortOf(trunk, 8)
|
|
c.Took = "4m12s"
|
|
c.Beside = map[string]string{"mesh-catalog": "0123456789ab"}
|
|
plan := *c.Plan
|
|
plan.Head, plan.Trunk = sha, c.Trunk
|
|
c.Plan = &plan
|
|
return c
|
|
}
|
|
|
|
func TestATrunkMoveSendsAReadyDeliveryBackToProposed(t *testing.T) {
|
|
const host = "novox/mesh-host"
|
|
const h64, h66, h67 = "6464646464646464", "dfb1415dfb1415df", "6767676767676767"
|
|
w := newWorld(t)
|
|
for _, p := range []PullEvent{pr(host, 64, h64, "a"), pr(host, 66, h66, "b"), pr(host, 67, h67, "c")} {
|
|
w.h.PullUpdated(p)
|
|
}
|
|
w.h.Checked(judged(host, 64, h64, "pass", trunk0))
|
|
w.h.Checked(judged(host, 66, h66, "pass", trunk0))
|
|
w.h.Checked(judged(host, 67, h67, "fail", trunk0))
|
|
id66, id67 := IDOf(host, h66), IDOf(host, h67)
|
|
if s := w.state(id66); s != Ready {
|
|
t.Fatalf("#66 checked green against the trunk is %s", s)
|
|
}
|
|
asked := len(w.ctl.checks)
|
|
|
|
// #64 merges: the trunk moves to its merge commit.
|
|
w.h.PullMerged(merged(host, 64, h64, merge1))
|
|
w.settleAll()
|
|
d := w.delivery(id66)
|
|
if d.State != Proposed {
|
|
t.Fatalf("#66's verdict names trunk %s, the trunk is now %s, and it is %s", trunk0[:12], merge1[:12], d.State)
|
|
}
|
|
last := d.Transitions[len(d.Transitions)-1]
|
|
if last.Event != EvTrunkMoved || !strings.Contains(last.Why, trunk0[:12]) || !strings.Contains(last.Why, merge1[:12]) {
|
|
t.Fatalf("its transition was %s, %q: not trunk moved with both commits", last.Event, last.Why)
|
|
}
|
|
if d.Check == nil || d.Check.Trunk != trunk0[:12] {
|
|
t.Fatalf("its out-of-date verdict was not kept to be shown: %+v", d.Check)
|
|
}
|
|
if s := w.state(id67); s != Rejected {
|
|
t.Fatalf("a rejected delivery went to %s on the trunk's move", s)
|
|
}
|
|
if len(w.ctl.checks) != asked {
|
|
t.Fatalf("the trunk's move asked a check: %v", w.ctl.checks[asked:])
|
|
}
|
|
|
|
// Out of date and not asked: no bound, so stalled does not list it, however long it waits.
|
|
w.later(5 * time.Hour)
|
|
w.settleAll()
|
|
for _, l := range w.h.Stalled() {
|
|
if l.ID == id66 {
|
|
t.Fatalf("stalled lists a proposed delivery whose check is out of date and not asked: %+v", l)
|
|
}
|
|
}
|
|
if len(w.ctl.checks) != asked {
|
|
t.Fatalf("an out-of-date check was asked by this owner: %v", w.ctl.checks[asked:])
|
|
}
|
|
|
|
// A late verdict for the old trunk is kept, does not count, and moves nothing: the table's fixed point.
|
|
before := len(w.delivery(id66).Transitions)
|
|
late := judged(host, 66, h66, "pass", trunk0)
|
|
late.ID = "check-late"
|
|
w.h.Checked(late)
|
|
w.settleAll()
|
|
d = w.delivery(id66)
|
|
if d.State != Proposed || len(d.Transitions) != before || d.Check == nil {
|
|
t.Fatalf("a late verdict for an old trunk left it %s after %d transition(s), verdict %+v", d.State,
|
|
len(d.Transitions)-before, d.Check)
|
|
}
|
|
if ts := settle(d, Facts{Now: w.now}); len(ts) != 0 {
|
|
t.Fatalf("the table took %d more transition(s) on a verdict that does not count", len(ts))
|
|
}
|
|
|
|
// The check asked again judges the head merged into the trunk's head: it counts.
|
|
w.h.Checked(judged(host, 66, h66, "pass", merge1))
|
|
w.settleAll()
|
|
if s := w.state(id66); s != Ready {
|
|
t.Fatalf("a verdict judged against the trunk's head left it %s", s)
|
|
}
|
|
}
|
|
|
|
// A rejected delivery judged again against another trunk — the merge tool's ask — is taken as its recheck.
|
|
func TestARejectedDeliveryJudgedAgainstANewTrunkIsRecheckedByTheVerdict(t *testing.T) {
|
|
const repo = "novox/mesh-lab"
|
|
const head = "1a2b3c4d5e6f7a8b"
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr(repo, 9, head, "x"))
|
|
w.h.Checked(judged(repo, 9, head, "fail", trunk0))
|
|
id := IDOf(repo, head)
|
|
if s := w.state(id); s != Rejected {
|
|
t.Fatalf("it is %s", s)
|
|
}
|
|
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
|
|
w.h.Checked(judged(repo, 9, head, "pass", merge1))
|
|
w.settleAll()
|
|
d := w.delivery(id)
|
|
if d.State != Ready || d.Check.Trunk != merge1[:12] {
|
|
t.Fatalf("judged green against the trunk's head it is %s with %+v", d.State, d.Check)
|
|
}
|
|
}
|
|
|
|
// **A verdict names the trunk commit it judged** in `checks` and `show`, and the kept plan records it (ADR 0323 §3).
|
|
func TestChecksAndShowNameTheHeadAndTheTrunkCommit(t *testing.T) {
|
|
const repo = "novox/mesh-lab"
|
|
const head = "1a2b3c4d5e6f7a8b"
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr(repo, 9, head, "x"))
|
|
w.h.Checked(judged(repo, 9, head, "pass", trunk0))
|
|
id := IDOf(repo, head)
|
|
|
|
a, err := w.h.Checks(repo, 0, head)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "head " + head[:12] + " merged into trunk " + trunk0[:12]
|
|
if a.Judged != want || a.Verdict == nil || a.Verdict.Head != head[:12] || a.Verdict.Trunk != trunk0[:12] {
|
|
t.Fatalf("checks says %q, verdict %+v; want %q", a.Judged, a.Verdict, want)
|
|
}
|
|
if !strings.Contains(a.Says, want) {
|
|
t.Fatalf("checks' words do not say what was judged: %s", a.Says)
|
|
}
|
|
if a.Verdict.Beside["mesh-catalog"] != "0123456789ab" || a.Verdict.Took != "4m12s" {
|
|
t.Fatalf("checks does not name the other repositories' commits or how long it took: %+v", a.Verdict)
|
|
}
|
|
shown, err := w.h.Show(id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := shown.(map[string]any)["delivery"].(*Delivery)
|
|
if d.Check.Head != head[:12] || d.Check.Trunk != trunk0[:12] {
|
|
t.Fatalf("show gives the verdict %+v", d.Check)
|
|
}
|
|
if d.Plan == nil || d.Plan.Trunk != trunk0[:12] || d.Plan.Head != head {
|
|
t.Fatalf("the kept delivery plan records %+v", d.Plan)
|
|
}
|
|
|
|
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
|
|
if a, _ = w.h.Checks(repo, 0, head); !strings.Contains(a.OutOfDate, merge1[:12]) {
|
|
t.Fatalf("checks does not say the verdict is out of date: %+v", a)
|
|
}
|
|
}
|
|
|
|
// A head whose merge check the forge's holder put out of date is not a check asked and not answered: `stalled` does
|
|
// not list it, however long it waits — rejected, or with no delivery here — until its check is asked (ADR 0323 §7).
|
|
func TestAnOutOfDateCheckOnTheForgeIsNotSaidUnanswered(t *testing.T) {
|
|
const repo = "novox/mesh-lab"
|
|
const head = "1a2b3c4d5e6f7a8b9c0d"
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr(repo, 9, head, "x"))
|
|
w.h.Checked(judged(repo, 9, head, "fail", trunk0))
|
|
words := outOfDateWords(trunk0, merge1)
|
|
pending := func(description, at string) map[string]HeadCheck {
|
|
return map[string]HeadCheck{ctxGate: {State: "pending", Description: description, UpdatedAt: at},
|
|
ctxRepo: {State: "pending", Description: description, UpdatedAt: at}}
|
|
}
|
|
w.forge.open = []OpenPull{{Owner: "novox", Repo: "mesh-lab", Number: 9, Base: "main", HeadSHA: head,
|
|
Required: []string{ctxGate, ctxRepo}, Statuses: []string{ctxGate, ctxRepo}, Checks: pending(words, "2026-10-06T12:01:00Z")}}
|
|
w.h.SeeOpenPulls()
|
|
w.later(3 * time.Hour)
|
|
w.h.SeeOpenPulls()
|
|
for _, l := range w.h.Stalled() {
|
|
if strings.HasPrefix(l.ID, repo) {
|
|
t.Fatalf("an out-of-date check nobody asked is said stalled: %+v", l)
|
|
}
|
|
}
|
|
// Asked by the merge tool: pending anew, and its bound runs from then.
|
|
w.forge.open[0].Checks = pending("waiting in the build queue since 15:01: asked by the merge tool", "2026-10-06T15:01:00Z")
|
|
w.h.SeeOpenPulls()
|
|
w.later(2 * time.Hour)
|
|
w.h.SeeOpenPulls()
|
|
said := false
|
|
for _, l := range w.h.Stalled() {
|
|
said = said || l.State == Unanswered
|
|
}
|
|
if !said {
|
|
t.Fatal("a check the merge tool asked and that never answered is not said")
|
|
}
|
|
}
|
|
|
|
// The merge tool's ask announces an out-of-date head again: its one-hour bound runs from then, and a check that never
|
|
// answers is said stalled (ADR 0323 §7).
|
|
func TestAnOutOfDateDeliveryAskedAgainIsBoundFromTheAsk(t *testing.T) {
|
|
const repo = "novox/mesh-lab"
|
|
const head = "1a2b3c4d5e6f7a8b"
|
|
w := newWorld(t)
|
|
w.h.PullUpdated(pr(repo, 9, head, "x"))
|
|
w.h.Checked(judged(repo, 9, head, "pass", trunk0))
|
|
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
|
|
id := IDOf(repo, head)
|
|
w.later(3 * time.Hour)
|
|
w.h.PullUpdated(pr(repo, 9, head, "x")) // the merge tool asked its check
|
|
w.later(30 * time.Minute)
|
|
for _, l := range w.h.Stalled() {
|
|
if l.ID == id {
|
|
t.Fatalf("listed half an hour after its check was asked again: %+v", l)
|
|
}
|
|
}
|
|
w.later(time.Hour)
|
|
said := false
|
|
for _, l := range w.h.Stalled() {
|
|
if l.ID == id {
|
|
said = true
|
|
if !strings.Contains(l.Says, "asked again") || !strings.Contains(l.H2, "operator") {
|
|
t.Fatalf("its stalled line says %+v", l)
|
|
}
|
|
}
|
|
}
|
|
if !said {
|
|
t.Fatal("a check asked again that never answered is not said")
|
|
}
|
|
// Its verdict judged against the trunk's head: ready, and nothing bounds it as proposed.
|
|
w.h.Checked(judged(repo, 9, head, "pass", merge1))
|
|
if d := w.delivery(id); d.State != Ready || d.AskedAgain != nil {
|
|
t.Fatalf("judged again it is %s, asked again %v", d.State, d.AskedAgain)
|
|
}
|
|
}
|
|
|
|
// A check this owner asks — a recheck — sends the base head the forge last announced the head with, so the
|
|
// controller's verdict for a head no build judges names its trunk commit (ADR 0323 §3, review of mesh-controller#271).
|
|
func TestARecheckSendsTheAnnouncedBaseHead(t *testing.T) {
|
|
const repo = "novox/mesh-tools-go"
|
|
const head = "1a2b3c4d5e6f7a8b"
|
|
w := newWorld(t)
|
|
p := pr(repo, 3, head, "x")
|
|
p.BaseSHA = trunk0
|
|
w.h.PullUpdated(p)
|
|
id := IDOf(repo, head)
|
|
if m := MemberOf(w.delivery(id)); m.BaseSHA != trunk0 {
|
|
t.Fatalf("the member sends base head %q", m.BaseSHA)
|
|
}
|
|
p.BaseSHA = merge1 // announced again, by the merge tool
|
|
w.h.PullUpdated(p)
|
|
if m := MemberOf(w.delivery(id)); m.BaseSHA != merge1 {
|
|
t.Fatalf("announced again, the member sends base head %q", m.BaseSHA)
|
|
}
|
|
if raw := string(mustJSON(MemberOf(w.delivery(id)))); !strings.Contains(raw, `"base_sha":"`+merge1+`"`) {
|
|
t.Fatalf("the member as the controller reads it: %s", raw)
|
|
}
|
|
}
|