mesh/merge-gate pass: builds mesh-delivery, mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery-group group fix/403-fix-time ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
hq ADR 0313: the operator's five minutes start when an agent moves the issue to fixing, not at the merge. mesh-delivery says on each transition, and in checks, when the pull request opened, when it merged and the class of what it moves. mesh-issues keeps the first move to fixing, the fix time with its four phases once the last fix is delivered, serves fix-times, and says issue-tracker.fix.<class>.over-budget on the operator's channel, cleared by the next fix of that class within budget.
180 lines
6.5 KiB
Go
180 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The manifest says what the code does (novox/hq to-be 53 §1): the seat it declares and holds once for the
|
|
// mesh with the verbs it serves, the events it says, the two states it keeps (made by the controller, never
|
|
// by this module), and its settings: the first number and the registered owners.
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Seats []seat `json:"seats"`
|
|
Claims []seat `json:"claims"`
|
|
Emits []string `json:"emits"`
|
|
Consumes []string `json:"consumes"`
|
|
Invokes []string `json:"invokes"`
|
|
State []json.RawMessage `json:"state"`
|
|
Logs []string `json:"logs"`
|
|
Tools []string `json:"tools"`
|
|
Resources []struct {
|
|
ID string `json:"id"`
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
} `json:"resources"`
|
|
Build struct {
|
|
Artifacts []struct {
|
|
From string `json:"from"`
|
|
Loads []string `json:"loads"`
|
|
Env map[string]string `json:"env"`
|
|
} `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
|
|
type seat struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves"`
|
|
}
|
|
|
|
type stateDeclaration struct {
|
|
Name string `json:"name"`
|
|
History int `json:"history"`
|
|
TTLSeconds int `json:"ttl-seconds"`
|
|
}
|
|
|
|
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 (m manifest) stateDeclarations(t *testing.T) []stateDeclaration {
|
|
t.Helper()
|
|
var out []stateDeclaration
|
|
for _, s := range m.State {
|
|
var d stateDeclaration
|
|
if json.Unmarshal(s, &d.Name) != nil {
|
|
if err := json.Unmarshal(s, &d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
out = append(out, d)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
want := []seat{{Name: Seat, Scope: "mesh", Serves: Verbs}}
|
|
if m.Module != "mesh-issues" || !reflect.DeepEqual(m.Seats, want) || !reflect.DeepEqual(m.Claims, want) {
|
|
t.Fatalf("module %q, seats %+v, claims %+v", m.Module, m.Seats, m.Claims)
|
|
}
|
|
if !reflect.DeepEqual(m.Emits, []string{EventOpened, EventMoved, EventNoted, EventLinked, EventImported, EventSet,
|
|
EventTriaged, EventReady, EventReadyToVerify, EventReserved}) {
|
|
t.Fatalf("emits %v", m.Emits)
|
|
}
|
|
// The record is the log `changes` (ADR 0300 §1). Four states, none expiring: issues the fold of the log (§3),
|
|
// still keeping its older values until the carry has read them (they go to one value in a follow-up), the
|
|
// counters one value each, the backlog's pass of the duplicate check one value (ADR 0278 §6), each reserved
|
|
// number one value (issue 424).
|
|
if !reflect.DeepEqual(m.Logs, []string{ChangesLog}) {
|
|
t.Fatalf("logs %v", m.Logs)
|
|
}
|
|
d := m.stateDeclarations(t)
|
|
if len(d) != 4 || d[0].Name != IssuesState || d[0].History < 2 || d[0].TTLSeconds != 0 ||
|
|
d[1].Name != CountersState || d[1].History > 1 || d[1].TTLSeconds != 0 ||
|
|
d[2].Name != GroomingState || d[2].History > 1 || d[2].TTLSeconds != 0 ||
|
|
d[3].Name != ReservationsState || d[3].History > 1 || d[3].TTLSeconds != 0 {
|
|
t.Fatalf("state %+v", d)
|
|
}
|
|
// The duplicate check hears the tracker's own opens and asks the agent module on its node (ADR 0278 §1, §5);
|
|
// the import and a reservation read the record through the records module (issue 424).
|
|
if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery.transition"}) ||
|
|
!reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify",
|
|
"gitea.gitea_pull_reviews", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
|
|
t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes)
|
|
}
|
|
if len(m.Build.Artifacts) != 1 || m.Build.Artifacts[0].From != "cmd/mesh-issues" ||
|
|
m.Build.Artifacts[0].Env["MESH_ISSUES_SETTINGS"] == "" || !reflect.DeepEqual(m.Build.Artifacts[0].Loads, []string{"mesh-issues"}) {
|
|
t.Fatalf("build %+v", m.Build)
|
|
}
|
|
}
|
|
|
|
func TestTheToolsAreTheSeatsVerbsAndSayWhatTheyRefuse(t *testing.T) {
|
|
m, _ := readManifest(t)
|
|
var verbs []string
|
|
for _, tool := range Tools(&Tracker{}) {
|
|
s, verb, ok := strings.Cut(tool.Name, ".")
|
|
if !ok || s != Seat {
|
|
t.Errorf("%s is not a verb of %s", tool.Name, Seat)
|
|
}
|
|
verbs = append(verbs, verb)
|
|
if len(tool.Description) < 80 {
|
|
t.Errorf("%s says too little of what it does", tool.Name)
|
|
}
|
|
for _, internal := range []string{"ADR", "to-be", "bucket", "compare-and-set", "JetStream", "KV"} {
|
|
if strings.Contains(tool.Description, internal) {
|
|
t.Errorf("%s's description uses the internal name %q", tool.Name, internal)
|
|
}
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(verbs, m.Claims[0].Serves) || len(m.Tools) != 0 {
|
|
t.Fatalf("verbs %v, claim %v, own tools %v", verbs, m.Claims[0].Serves, m.Tools)
|
|
}
|
|
}
|
|
|
|
// The settings the manifest starts with: the first number, and a registered list of owners in which every
|
|
// module named is one the catalogue holds.
|
|
func TestTheSettingsStartTheCounterAndRegisterTheOwners(t *testing.T) {
|
|
m, raw := readManifest(t)
|
|
var content string
|
|
for _, r := range m.Resources {
|
|
if r.ID == "settings" {
|
|
content = r.Content
|
|
}
|
|
}
|
|
var s Settings
|
|
if err := json.Unmarshal([]byte(content), &s); err != nil {
|
|
t.Fatalf("the settings the manifest writes are not the module's: %v", err)
|
|
}
|
|
if s.FirstNumber < 365 {
|
|
t.Fatalf("first-number %d: the first issue continues hq's numbering, after 364", s.FirstNumber)
|
|
}
|
|
if !sort.StringsAreSorted(s.Owners) {
|
|
t.Error("the owners are not sorted")
|
|
}
|
|
for _, repo := range []string{"hq", "mesh-catalog", "mesh-controller", "mesh-host", "mesh-sdk", "mesh-tools"} {
|
|
if Owners(s.Owners).owns(repo) != repo {
|
|
t.Errorf("%s is not registered", repo)
|
|
}
|
|
}
|
|
modules := filepath.Join("..", "..", "..")
|
|
for _, o := range s.Owners {
|
|
if name, ok := strings.CutPrefix(o, "mesh-catalog modules/"); ok {
|
|
if _, err := os.Stat(filepath.Join(modules, name, "module.json")); err != nil {
|
|
t.Errorf("the owner %q names a module the catalogue does not hold", o)
|
|
}
|
|
}
|
|
}
|
|
for _, never := range []string{"/home/", "jochen", "shanks", "novox.be"} {
|
|
if strings.Contains(strings.ToLower(raw), never) {
|
|
t.Errorf("module.json names %q", never)
|
|
}
|
|
}
|
|
}
|