Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go
T
jschoubben e53ac973e9 mesh-delivery-gate: stop asking the controller's forget-checks (issue 1318, the gate's part)
The controller's forget-checks goes with the merge gate's checks (ADR 0503 §3)
and 1318 removes the verb in the same group, so the gate must stop calling it
first: a closed pull request is stopped and asks the controller nothing, and
the manifest no longer invokes it. delivery-check stays for hq's base until
R4g (the lead's decision for 1302).
2026-10-22 17:50:44 +02:00

203 lines
7.6 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"slices"
"sort"
"strings"
"testing"
"time"
)
// The manifest says what the code does: the seat it claims and the verbs it serves, the events it follows
// and says, the tools it calls, its state — and names nothing of one installation.
type manifest struct {
Module string `json:"module"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Seats []any `json:"seats"`
Uses []string `json:"uses"`
Consumes []string `json:"consumes"`
Emits []string `json:"emits"`
Invokes []string `json:"invokes"`
State []any `json:"state"`
Reads []string `json:"reads"`
Tools []string `json:"tools"`
}
// buckets are the names of the state the manifest declares, each a bare name or an object with a name and a history.
func (m manifest) buckets() []string {
var out []string
for _, s := range m.State {
switch v := s.(type) {
case string:
out = append(out, v)
case map[string]any:
out = append(out, v["name"].(string))
}
}
return out
}
func readManifest(t *testing.T) (manifest, string) {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m, string(raw)
}
func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
m, raw := readManifest(t)
if m.Module != "mesh-delivery-gate" || len(m.Claims) != 1 || m.Claims[0].Name != "mesh-delivery" || m.Claims[0].Scope != "mesh" {
t.Fatalf("claims %+v", m.Claims)
}
// The seat is the mesh's own (mesh-*): a module claims it and never declares it.
if len(m.Seats) != 0 {
t.Fatal("the manifest declares a seat; mesh-delivery is the mesh's own")
}
// The thirteen verbs of the seat (ADR 0437 §3, §11).
if len(m.Claims[0].Serves) != 13 {
t.Fatalf("serves %d verbs, wanted 13: %v", len(m.Claims[0].Serves), m.Claims[0].Serves)
}
if !reflect.DeepEqual(m.Consumes, followed) || !slices.Contains(m.Consumes, "mesh-delivery-forge-bridge.forge.**") {
t.Fatalf("consumes %v, follows %v (the bridge's forge messages among them)", m.Consumes, followed)
}
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
t.Fatalf("emits %v", m.Emits)
}
// Seven buckets, and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3.
buckets := m.buckets()
if !reflect.DeepEqual(buckets, []string{"deliveries", "groups", "pulls", "asks", "merges", "owed", "source"}) {
t.Fatalf("state %v", buckets)
}
// What it reads: the carry from today's module's three buckets, the teller's answers, the checker's verdicts (issue
// 1302), the seat's holder and the machines' haves (ADR 0450 §6 and its insight of 2026-10-20).
if !reflect.DeepEqual(m.Reads, []string{"mesh-delivery.deliveries", "mesh-delivery.groups", "mesh-delivery.pulls",
"mesh-delivery-teller.answers", "mesh-delivery-checker.verdicts", "mesh-controller.assignments", "node-engine.have"}) {
t.Fatalf("reads %v", m.Reads)
}
// It calls exactly the controller's seven verbs (ADR 0450 §7; queue since issue 1274, forget-checks no more since issue 1318) and reads the forge with
// three tools; the forge's
// writes are the bridge's (ADR 0437 §8). It uses no operator-channel: the teller asks (ADR 0437 §6).
want := []string{}
for _, v := range []string{"deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-check", "queue", "plans"} {
want = append(want, "seat:"+ControllerSeat+"."+v)
}
want = append(want, "gitea.gitea_commit_statuses", "gitea.gitea_contains", "gitea.gitea_open_pulls")
if len(m.Uses) != 0 {
t.Fatalf("uses %v: the teller asks, not the gate", m.Uses)
}
if !reflect.DeepEqual(m.Invokes, want) {
t.Fatalf("invokes %v, wanted %v", m.Invokes, want)
}
for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox", "zurag", "anchor"} {
if strings.Contains(strings.ToLower(raw), never) {
t.Errorf("module.json names %q", never)
}
}
}
func TestTheToolsAgreeWithTheManifest(t *testing.T) {
m, _ := readManifest(t)
var own, verbs []string
for _, tool := range tools(&Holder{}, &listening{}) {
if strings.TrimSpace(tool.Description) == "" {
t.Errorf("%s has no description", tool.Name)
}
if seat, verb, ok := strings.Cut(tool.Name, "."); ok {
if seat != "mesh-delivery" {
t.Errorf("%s is a verb of a seat this does not hold", tool.Name)
}
verbs = append(verbs, verb)
continue
}
own = append(own, tool.Name)
}
sort.Strings(own)
if !reflect.DeepEqual(own, m.Tools) {
t.Fatalf("serves %v, lists %v", own, m.Tools)
}
if !reflect.DeepEqual(verbs, m.Claims[0].Serves) {
t.Fatalf("verbs %v, claim %v", verbs, m.Claims[0].Serves)
}
}
// The forge's and the controller's answers are read whatever wraps them.
func TestAnAnswerIsReadWhateverWrapsIt(t *testing.T) {
for _, c := range []struct {
raw string
ok bool
answer string
}{
{`{"output":"x","ok":true,"answer":{"held":true}}`, true, `{"held":true}`},
{`{"output":"refused: no plan","ok":false}`, false, ``},
{`"{\"output\":\"x\",\"ok\":true,\"answer\":[1]}"`, true, `[1]`},
{`{"content":[{"type":"text","text":"{\"output\":\"x\",\"ok\":true,\"answer\":2}"}]}`, true, `2`},
{`{"content":[{"type":"text","text":"boom"}],"isError":true}`, false, ``},
} {
answer, _, ok := answerOf(json.RawMessage(c.raw))
if ok != c.ok || (c.ok && string(answer) != c.answer) {
t.Errorf("%s → %s %v", c.raw, answer, ok)
}
}
}
// **No ready mark is left in the module** (novox/hq ADR 0421 §4, issue 1022; how it is checked): no label, comment, verb
// or trusted setting of the mark stands in any of its code or its manifest. Tests are left out: they name the words to
// prove them gone.
func TestNoReadyMarkIsLeftInTheModule(t *testing.T) {
forbidden := []string{"mesh/ready", "ready at", "ready-authors", "mark-ready", "ReadyLabel", "pull-request.labeled",
"pull-request.unlabeled", "pull-request.commented"}
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
files = append(files, filepath.Join("..", "..", "module.json"))
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
raw, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
for _, word := range forbidden {
if strings.Contains(string(raw), word) {
t.Errorf("%s still holds %q: the ready mark is gone (ADR 0421 §4)", f, word)
}
}
}
}
// A row written while the ready mark stood still reads: its mark's fields are ignored, its ask and merge kept (ADR 0421
// §4 consequences, issue 1022).
func TestARowWrittenWithTheReadyMarkStillReads(t *testing.T) {
store := newMemStore()
row := `{"id":"novox/app@111111111111","repository":"novox/app","commit":"` + readyHead + `","number":7,"base":"main",
"state":"ready","since":"2026-10-17T10:00:00Z","created":"2026-10-17T10:00:00Z","let_go":"0001-01-01T00:00:00Z",
"ready":{"head":"` + readyHead + `","by":"lead@shanks","marked_at":"2026-10-17T10:01:00Z","seen":"2026-10-17T10:02:00Z",
"last_try":"0001-01-01T00:00:00Z","asked_at":"2026-10-17T10:03:00Z","ask_id":"merge-7-111111111111-1","heads":{"novox/app@111111111111":"` + readyHead + `"}}}`
store.deliveries["novox/app@111111111111"] = []byte(row)
w := newWorldOver(t, store, newFakeController(), newFakeForge(), time.Date(2026, 10, 17, 11, 0, 0, 0, time.UTC))
d := w.delivery("novox/app@111111111111")
if d.Ready == nil || d.Ready.AskID != "merge-7-111111111111-1" || d.Ready.Head != readyHead {
t.Fatalf("the old row is not read: %+v", d.Ready)
}
if !MemberOf(d).Ready {
t.Fatalf("an ask waiting is a ready member")
}
}