Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1349_test.go
T
jschoubben fb89990e2b mesh-delivery-gate: ask a check the build queue lost again, and take a recheck from proposed when none is held (issue 1274)
A proposed delivery whose asked check no queue entry holds stayed proposed for good (met in 1311, 1336, 1726, 1792, 1795, 1833). The sweep reads the controller's queue and asks again past max(10 minutes, the repository's last check time); recheck from proposed is taken on the same fact; never while the queue holds one.
2026-10-22 16:34:33 +02:00

232 lines
8.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"slices"
"strconv"
"strings"
"sync"
"testing"
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
)
// **The gate writes nothing to the forge: its statuses, views, notes and comments are owed keys, and its plan is read from
// `plans`** (novox/hq ADR 0437 §2, §8, ADR 0450 §2, §4, §7, issue 1349; the empty check answer is the mesh-delivery half of
// issue 1311). Fixture sources: the key and the value of an owed act are mesh-sdk go/delivery's (OwedKey, Owed); the arguments
// of `plans` are mesh-controller cmd/mesh-controller/seatverbs.go's (repository, paths, module-dirs, head, base, removed, data)
// and its answer the document of deliveryPlanDocument (plan, reach, mesh, gated).
// AC1: a transition that today sets a status, a view, a note or a comment writes the matching owed key.
func TestATransitionWritesItsStatusViewAndNoteAsOwedKeys(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.settleAll()
page := "https://forge.invalid/novox/app/pulls/7"
statusKey, _ := sdk.OwedKey("novox/app", 7, head, "status=mesh/delivery")
viewKey, _ := sdk.OwedKey("novox/app", 7, head, "view")
noteKey, _ := sdk.OwedKey("novox/app", 7, head, "note")
for _, key := range []string{statusKey, viewKey, noteKey} {
if _, there := w.owed.keys[key]; !there {
t.Fatalf("the key %s was not written; the keys are %v", key, w.owed.writes)
}
}
if s := w.owed.keys[statusKey]; s.Act != sdk.OwedStatus || s.Context != "mesh/delivery" || s.State != "pending" || s.Target != page || s.Text == "" {
t.Fatalf("the owed status is %+v; wanted mesh/delivery pending, pointing at %s", s, page)
}
if v := w.owed.keys[viewKey]; v.Act != sdk.OwedView || !strings.Contains(v.Text, id) {
t.Fatalf("the owed view is %+v", v)
}
if n := w.owed.keys[noteKey]; n.Act != sdk.OwedNote || n.Ref != "mesh-plan" || !strings.Contains(n.Text, "proposed") {
t.Fatalf("the owed note is %+v", n)
}
// What the bridge would perform is what the forge holds.
if len(w.forge.notesOn(head)) == 0 || !strings.HasPrefix(statusOn(w, "novox/app", head), "pending") {
t.Fatalf("the fake bridge did not perform what was owed: %v", w.forge.notesOn(head))
}
}
// AC1: a note on the merge commit, which no pull request names, is keyed with the number 0.
func TestANoteOnTheMergeCommitIsOwedWithNumberZero(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.h.WalkMoved(w.ctl.put(aWalk("plan-1", "novox/app", merge, true)))
w.settleAll()
key, _ := sdk.OwedKey("novox/app", 0, merge, "note")
if n, there := w.owed.keys[key]; !there || n.Act != sdk.OwedNote {
t.Fatalf("no note was owed on the merge commit under %s; the keys are %v", key, w.owed.writes)
}
}
// AC1: two comments on one head are two acts: the sequence number is in the act.
func TestTwoCommentsOnOneHeadAreTwoOwedKeys(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.mu.Lock()
d := w.h.deliveries[id]
w.h.sayOn(d, "the first thing said")
w.h.sayOn(d, "the second thing said")
w.h.mu.Unlock()
w.settleAll()
one, _ := sdk.OwedKey("novox/app", 7, head, "comment.1")
two, _ := sdk.OwedKey("novox/app", 7, head, "comment.2")
if w.owed.keys[one].Text != "the first thing said" || w.owed.keys[two].Text != "the second thing said" {
t.Fatalf("the comments are %+v and %+v", w.owed.keys[one], w.owed.keys[two])
}
if len(w.forge.comments) != 2 {
t.Fatalf("the bridge would say %d comments: %v", len(w.forge.comments), w.forge.comments)
}
}
// AC2: the gate's code calls no gitea write tool and no `delivery-plan`, and no controller verb beyond the ones its manifest
// names (ADR 0450 §7). Tests are left out: they name the words to prove them gone.
func TestTheGatesCodeCallsNoForgeWriteToolAndNoDeliveryPlan(t *testing.T) {
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
forbidden := []string{"delivery-plan", "gitea_commit_status\"", "gitea_delivery_view", "gitea_note_append", "gitea_add_comment",
"gitea_merge_pull_request\"", "gitea_open_pulls_write"}
// The controller's verbs the gate calls: ADR 0450 §7.
allowed := []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "forget-checks", "plans", "queue"}
called := map[string]bool{}
fset := token.NewFileSet()
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
file, err := parser.ParseFile(fset, f, nil, 0)
if err != nil {
t.Fatal(err)
}
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.BasicLit:
if x.Kind == token.STRING {
for _, word := range forbidden {
if strings.Contains(x.Value, word) {
t.Errorf("%s holds the string %s", f, x.Value)
}
}
}
case *ast.CallExpr:
if sel, ok := x.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "verb" && len(x.Args) > 0 {
if lit, ok := x.Args[0].(*ast.BasicLit); ok {
verb, _ := strconv.Unquote(lit.Value)
called[verb] = true
if !slices.Contains(allowed, verb) {
t.Errorf("%s calls the controller's verb %q", f, verb)
}
}
}
}
return true
})
}
for _, verb := range []string{"plans", "delivery-check"} {
if !called[verb] {
t.Errorf("the gate calls no %s: the test reads nothing", verb)
}
}
}
// stubController is a controller that answers every verb with one document, and keeps what it was asked.
type stubController struct {
mu sync.Mutex
keys []string
bodies []map[string]any
answer string
}
func (s *stubController) ask(key string, body any) (json.RawMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.keys = append(s.keys, key)
b, _ := body.(map[string]any)
s.bodies = append(s.bodies, b)
return json.RawMessage(s.answer), nil
}
func (s *stubController) called(key string) []map[string]any {
s.mu.Lock()
defer s.mu.Unlock()
var out []map[string]any
for i, k := range s.keys {
if k == key {
out = append(out, s.bodies[i])
}
}
return out
}
// AC3: a delivery with no plan gets it from `plans`, asked as data, with the change's diffset.
func TestADeliveryWithNoPlanGetsItFromPlansAsData(t *testing.T) {
w := newWorld(t)
stub := &stubController{answer: `{"ok":true,"output":"","answer":{"asked":"check-1","held":true,"walks":[],
"plan":{"repository":"novox/app","base":"main","head":"` + head + `","moved":["app"],"summary":"builds app, sent to the node"}}}`}
w.h.Controller = seatController{ask: stub.ask}
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
c := verdict("novox/app", 7, head, "pass")
c.Plan = nil
w.h.Checked(c)
w.settleAll()
asked := stub.called("seat:mesh-controller.plans")
if len(asked) == 0 {
t.Fatalf("`plans` was not asked; the controller was asked %v", stub.keys)
}
if len(stub.called("seat:mesh-controller.delivery-plan")) != 0 {
t.Fatal("delivery-plan was asked")
}
got := asked[0]
if got["repository"] != "novox/app" || got["paths"] != "src/a.go" || got["data"] != "true" || got["head"] != head || got["base"] != "main" {
t.Fatalf("plans was asked %v; wanted the repository, the paths, the head, the base and data", got)
}
if d := w.delivery(IDOf("novox/app", head)); d.Plan == nil || d.Plan.Summary != "builds app, sent to the node" {
t.Fatalf("the delivery's plan is %+v", d.Plan)
}
}
// AC4: a delivery-check answer that holds no id is an error said aloud, naming the delivery, and not taken as asked.
func TestAnEmptyDeliveryCheckAnswerIsAnErrorNamingTheDelivery(t *testing.T) {
stub := &stubController{answer: `{"ok":true,"output":"","answer":{}}`}
if id, err := (seatController{ask: stub.ask}).Check("", []Member{{ID: "novox/app@x"}}); err == nil || id != "" ||
!strings.Contains(err.Error(), "no check id") {
t.Fatalf("an empty check answer gave %q and %v", id, err)
}
w := newWorld(t)
var said []string
var mu sync.Mutex
w.h.Logf = func(format string, a ...any) {
mu.Lock()
defer mu.Unlock()
said = append(said, strings.TrimSpace(fmt.Sprintf(format, a...)))
}
w.h.Controller = seatController{ask: stub.ask}
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
id := IDOf("novox/app", head)
mu.Lock()
defer mu.Unlock()
found := false
for _, line := range said {
if strings.Contains(line, id) && strings.Contains(line, "no check id") && !strings.Contains(line, "taken as refused") {
found = true
}
}
if !found {
t.Fatalf("no error naming %s and the empty answer was logged: %q", id, said)
}
if d := w.delivery(id); d.CheckAsk != nil {
t.Fatalf("an empty answer was kept as an ask: %+v", d.CheckAsk)
}
}