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).
203 lines
7.6 KiB
Go
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")
|
|
}
|
|
}
|