Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1826_test.go
T
jschoubben 260be1c3e6 mesh-delivery-gate: ask a pull request's check again when a config value is held or dropped for it (issue 1826)
mesh-config raises config-changed (hold, drop) naming the pull request; the delivery, or its group's
composed check, is asked again on the gate's own path, with why in the transition. Not the recheck verb.
2026-10-22 11:01:07 +02:00

146 lines
5.8 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Issue 1826 (novox/hq ADR 0502 clause 5): when mesh-config holds or drops a value for a pull request that is a
// delivery, its check, or its group's composed check, is asked again, and the transition says why.
func configChanged(action, pull string) ConfigChange {
c := ConfigChange{Action: action, Pull: pull, Head: head, Key: "media-gid"}
c.Level.Node, c.Level.Module = "ace", "n8n"
return c
}
// 1826/K4: a lone delivery that was ready is proposed again with its merge check pending, its check asked, and the
// transition says that a config value was held.
func TestAHeldValueAsksTheReadyDeliverysCheckAgain(t *testing.T) {
w := newWorld(t)
id := heldGreen(t, w, "novox/app", 7)
asked := len(w.ctl.checks)
w.h.ConfigChanged(configChanged("hold", "novox/app#7"))
if got := w.ctl.checks[asked:]; len(got) != 1 || got[0] != ":"+id {
t.Fatalf("asked %v; wanted its own check once", got)
}
d := w.delivery(id)
last := d.Transitions[len(d.Transitions)-1]
if d.State != Proposed || last.Event != EvRecheck || !strings.Contains(last.Why, "media-gid") ||
!strings.Contains(last.Why, "held") || last.By != byOwner {
t.Errorf("state %s, last transition %+v", d.State, last)
}
if s := w.forge.statuses["novox/app@"+head+" "+ctxGate]; !strings.HasPrefix(s, "pending checking again") {
t.Errorf("the merge check says %q: the old verdict would still let it merge", s)
}
}
// 1826/K4: a rejected delivery (the very case: the composed check failed on the missing value) is asked again on a
// hold; and a drop asks it again too.
func TestAHeldValueAsksARejectedDeliverysCheckAgainAndADropToo(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 8, head, "feat/y", "src/a.go"))
w.h.Checked(verdict("novox/app", 8, head, "fail"))
w.settleAll()
id := IDOf("novox/app", head)
if w.state(id) != Rejected {
t.Fatalf("it is %s", w.state(id))
}
asked := len(w.ctl.checks)
w.h.ConfigChanged(configChanged("hold", "novox/app#8"))
if w.state(id) != Proposed || len(w.ctl.checks) != asked+1 {
t.Fatalf("a hold left it %s with %v asked", w.state(id), w.ctl.checks[asked:])
}
w.h.Checked(verdict("novox/app", 8, head, "pass"))
w.settleAll()
asked = len(w.ctl.checks)
w.h.ConfigChanged(configChanged("drop", "novox/app#8"))
d := w.delivery(id)
if len(w.ctl.checks) != asked+1 || !strings.Contains(d.Transitions[len(d.Transitions)-1].Why, "dropped") {
t.Errorf("a drop asked %v; last transition %+v", w.ctl.checks[asked:], d.Transitions[len(d.Transitions)-1])
}
}
// 1826/K4: a delivery whose check is on its way is asked again, its old ask replaced, and says why in its note.
func TestAHeldValueAsksTheCheckOfAProposedDeliveryAgain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 9, head, "feat/z", "src/a.go"))
w.settleAll()
id := IDOf("novox/app", head)
before := w.delivery(id).CheckAsk
asked := len(w.ctl.checks)
w.h.ConfigChanged(configChanged("hold", "novox/app#9"))
d := w.delivery(id)
if len(w.ctl.checks) != asked+1 || d.CheckAsk == nil || (before != nil && d.CheckAsk.ID == before.ID) ||
!strings.Contains(d.CheckAsk.Why, "held") {
t.Errorf("asked %v; ask %+v (was %+v)", w.ctl.checks[asked:], d.CheckAsk, before)
}
if d.State != Proposed {
t.Errorf("it is %s", d.State)
}
}
// 1826/K4: a hold for a member of a group asks the member's check again and then the group's composed check, which
// the group waits for.
func TestAHeldValueForAGroupMemberAsksTheComposedCheckAgain(t *testing.T) {
w := approvedGroup(t)
g := w.h.groups["feat/g"]
firstComposed := g.Check.Asked
asked := len(w.ctl.checks)
w.h.ConfigChanged(configChanged("hold", gCat+"#2"))
if got := w.ctl.checks[asked:]; len(got) != 1 || got[0] != ":"+IDOf(gCat, gHk) {
t.Fatalf("a hold asked %v; wanted the member's own check", got)
}
if g = w.h.groups["feat/g"]; g.Check.OutOfDate == "" || !strings.Contains(g.Check.OutOfDate, "rechecked") {
t.Fatalf("the composed check stands: %+v", g.Check)
}
w.later(60e9)
w.h.Checked(judged(gCat, 2, gHk, "pass", gTrunk0))
w.settleAll()
last := w.ctl.checks[len(w.ctl.checks)-1]
if !strings.HasPrefix(last, "feat/g:") || w.h.groups["feat/g"].Check.Asked == firstComposed {
t.Errorf("the composed check was not asked again: %v", w.ctl.checks[asked:])
}
}
// 1826/K4: what is not a hold or a drop for a delivery of this owner asks nothing: the merge's set, another action,
// another pull request, a pull request with no delivery, and a delivery already merged.
func TestOnlyAHoldOrADropForADeliveryAsksACheckAgain(t *testing.T) {
w := newWorld(t)
heldGreen(t, w, "novox/app", 7)
asked := len(w.ctl.checks)
for _, c := range []ConfigChange{configChanged("set", "novox/app#7"), configChanged("resolve", ""),
configChanged("hold", "novox/app#99"), configChanged("hold", "novox/other#7"), configChanged("hold", ""),
configChanged("hold", "garbage")} {
w.h.ConfigChanged(c)
}
if len(w.ctl.checks) != asked || w.state(IDOf("novox/app", head)) != Ready {
t.Errorf("asked %v; it is %s", w.ctl.checks[asked:], w.state(IDOf("novox/app", head)))
}
}
// 1826/K4: the event is heard as mesh-config's config-changed, and it is no recheck verb.
func TestTheConfigChangedEventIsFollowed(t *testing.T) {
w := newWorld(t)
heldGreen(t, w, "novox/app", 7)
found := false
for _, f := range followed {
found = found || f == "mesh-config.config-changed"
}
if !found {
t.Fatalf("not followed: %v", followed)
}
raw, _ := json.Marshal(map[string]any{"action": "hold", "pull": "novox/app#7", "head": head, "key": "media-gid",
"level": map[string]any{"level": "module", "node": "ace", "module": "n8n"}})
asked := len(w.ctl.checks)
if err := handle(w.h, stdio.Envelope{Key: "mesh-config.config-changed", Body: raw}); err != nil {
t.Fatal(err)
}
if len(w.ctl.checks) != asked+1 {
t.Errorf("the event asked %v", w.ctl.checks[asked:])
}
}