Files
mesh-delivery/modules/mesh-delivery-checker/manifest_test.go
T
jschoubben 453cdfa607 The check waits on the builder's builds of its candidate before it composes, so it composes with them (ADR 0603 §10)
The builder already builds every candidate fingerprint into its own key. The check now watches exactly those keys on the
bus and composes once each is done or failed, within the new setting build-wait-bound (1800 s). A failed build fails the
check, naming it; a build not ended within the bound is named and the check composes without it. A module built by a
build module the change alters (mesh-build-go or mesh-build-node), one standing on a base the change alters, and one the
trunk holds no record of are not waited on, and the composition's warnings name each with why.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 17:51:09 +01:00

241 lines
10 KiB
Go

package manifest
import (
"bytes"
"encoding/json"
"io/fs"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
)
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
// Invokes :590, Streams :613, Reads :659; StreamDeclaration, streams.go), an internal package of another repository.
// These tests decode the manifest strictly into the fields those types name (DisallowUnknownFields) and hold the
// rules of issues 1678 and 1683 (ADR 0453 §4, ADR 0484). The merge gate's module check proves the controller takes the manifest.
type state struct {
Name string `json:"name"`
History int `json:"history,omitempty"`
}
type artifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Language string `json:"language"`
System string `json:"system"`
From string `json:"from"`
Binary string `json:"binary"`
Loads []string `json:"loads"`
}
type stream struct {
Name string `json:"name"`
Events []string `json:"events"`
}
type manifest struct {
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Streams []stream `json:"streams"`
Tools []string `json:"tools"`
Module string `json:"module"`
Version string `json:"version"`
Slug string `json:"slug"`
Consumes []string `json:"consumes"`
State []state `json:"state"`
Reads []string `json:"reads"`
Uses []string `json:"uses"`
Invokes []string `json:"invokes"`
Config map[string]struct {
Type string `json:"type"`
Description string `json:"description"`
Pattern string `json:"pattern"`
Default any `json:"default"`
Minimum *float64 `json:"minimum"`
Maximum *float64 `json:"maximum"`
Trusted bool `json:"trusted"`
} `json:"config"`
OwnSecrets map[string]string `json:"own-secrets"`
SecretsOwner string `json:"secrets-owner"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []artifact `json:"artifacts"`
} `json:"build"`
}
func load(t *testing.T) (manifest, map[string]json.RawMessage) {
t.Helper()
raw, err := os.ReadFile("module.json")
if err != nil {
t.Fatal(err)
}
var m manifest
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&m); err != nil {
t.Fatalf("the controller's reader refuses a key this manifest has: %v", err)
}
var top map[string]json.RawMessage
if err := json.Unmarshal(raw, &top); err != nil {
t.Fatal(err)
}
return m, top
}
func sorted(s []string) []string {
s = slices.Clone(s)
slices.Sort(s)
return s
}
func TestManifestDeclarations(t *testing.T) { // 1678/AC1
m, top := load(t)
if m.Module != "mesh-delivery-checker" {
t.Errorf("module = %q", m.Module)
}
// delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets.
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-node.steps", "mesh-build-node.toolchains",
"mesh-delivery-assigner.placement", "mesh-delivery-builder.builds", "mesh-delivery-gate.source", "mesh-delivery-gate.turns", "mesh-delivery-indexer.candidates",
"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-indexer.packages", "mesh-delivery-indexer.predefined", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want)
}
// 1887/A5b: the checker reads nothing of the controller's plans-building (ADR 0483 superseded by ADR 0525 §7), calls no
// build module's tool and holds no capacity: it puts check asks on the Go and Node build seats (ADR 0525 §1, §4).
// The forge's reads for a mesh-sdk pin (ADR 0484 clause 9), and of the node seat node-engine its one verb
// compose-candidate, asked of every placed node (ADR 0593 §7), never the seat's other verbs.
if want := []string{"gitea.gitea_commits", "gitea.gitea_compare", "gitea.gitea_get_file", "gitea.gitea_list_branches",
"seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
}
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
t.Errorf("uses = %v, want the Go and Node build seats", m.Uses)
}
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
if b, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 || b != "${dir:state}/broker" {
t.Errorf("own-secrets = %v, want exactly broker", m.OwnSecrets)
}
if m.SecretsOwner != "mesh-delivery-checker" {
t.Errorf("secrets-owner = %q", m.SecretsOwner)
}
// The checker says each time it wrote a preview or a status, so the indexer's and the forge bridge's watches find the buckets (ADR 0484 clauses 2 and 3).
if want := []stream{{Name: "checks", Events: []string{"preview.**", "owed.**"}}}; !reflect.DeepEqual(m.Streams, want) {
t.Errorf("streams = %+v, want %+v", m.Streams, want)
}
// The read tools (issue 1686) are served by the tools bundle, not by the checker's process.
// The try's tools (ADR 0501 §5; the end by hand, issue 734) beside them: mesh-controller's try calls them. A lost check
// is asked again by hand (checker_ask_again).
if want := []string{"checker_ask_again", "checker_kept", "checker_previews", "checker_pull", "checker_status", "checker_try", "checker_try_end", "checker_try_read", "checker_verdicts"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// The try is the checker's own seat's verbs (ADR 0547 §10, issue 774), served beside the tools by the same code.
if len(m.Claims) != 1 || m.Claims[0].Name != "mesh-delivery-checker" || m.Claims[0].Scope != "mesh" ||
!reflect.DeepEqual(m.Claims[0].Serves, []string{"try", "try-read", "try-end"}) {
t.Errorf("claims = %+v, want the mesh seat mesh-delivery-checker serving try, try-read and try-end", m.Claims)
}
for _, k := range []string{"logs", "emits", "provides", "requires", "defines-seats", "seat", "seats", "bus"} { // config: compose-answer-bound (ADR 0593 §7)
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the checker has none", k)
}
}
}
func TestManifestProcess(t *testing.T) { // 1678/AC1
m, _ := load(t)
var procs []map[string]any
for _, r := range m.Resources {
if r["type"] == "process" {
procs = append(procs, r)
}
if r["type"] == "tool" || r["type"] == "seat" {
t.Errorf("resource %v: the checker serves no tool and holds no seat", r)
}
}
if len(procs) != 1 {
t.Fatalf("%d process resources, want one", len(procs))
}
p := procs[0]
if p["artifact"] != "program" || !reflect.DeepEqual(p["run"], []any{"./mesh-delivery-checker"}) {
t.Errorf("process = %v", p)
}
env, _ := p["env"].(map[string]any)
if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_CHECKER_SETTINGS"] != "${dir:state}/settings.json" {
t.Errorf("env = %v, want the broker file and the settings file", p["env"])
}
// Its settings hold its answer bound (ADR 0593 §7: compose-answer-bound, 60 s, the checker's) and its wait for the
// candidate's builds (ADR 0603 §10: build-wait-bound, 1800 s), never a capacity of its own (ADR 0525 §4).
for _, r := range m.Resources {
if r["id"] == "settings" && !reflect.DeepEqual(r["takes-config"], []any{"compose-answer-bound", "build-wait-bound"}) {
t.Errorf("the settings resource %v takes other than compose-answer-bound and build-wait-bound", r)
}
}
if c, ok := m.Config["build-wait-bound"]; !ok || c.Default != float64(1800) || c.Trusted {
t.Errorf("build-wait-bound = %+v, want 1800 by default, not trusted", c)
}
if c, ok := m.Config["compose-answer-bound"]; !ok || c.Default != float64(60) || c.Trusted {
t.Errorf("compose-answer-bound = %+v, want 60 by default, not trusted", c)
}
if _, bus := p["bus"]; bus {
t.Errorf("the process declares a bus: it runs on its own broker credential")
}
// The settings template is packed as the files archive (novox/hq ADR 0349 §2), or the indexer's judge refuses it.
if len(m.Build.Artifacts) != 3 {
t.Fatalf("artifacts = %+v, want the program, the tools and the files archive", m.Build.Artifacts)
}
if a := m.Build.Artifacts[2]; a.Name != "files" || a.Kind != "archive" || a.From != "files" {
t.Errorf("files artifact = %+v, want an archive from files/", a)
}
if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Language != "go" || a.Binary != "mesh-delivery-checker-tools" ||
!reflect.DeepEqual(a.Loads, []string{"mesh-delivery-checker-tools"}) {
t.Errorf("tools artifact = %+v", a)
}
if st, err := os.Stat(m.Build.Artifacts[1].From); err != nil || !st.IsDir() {
t.Errorf("the tools' source %s: %v", m.Build.Artifacts[1].From, err)
}
a := m.Build.Artifacts[0]
if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-checker" || !strings.HasSuffix(a.From, a.Binary) || a.Kind != "bundle" || a.Language != "go" {
t.Errorf("artifact = %+v", a)
}
}
func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1678/AC6 (ADR 0437 §8: no timer, a wait is a context's bound)
forbidden := []string{"Subscribe(", "QueueSubscribe", "ChanSubscribe", "time.NewTicker", "time.Tick", "time.NewTimer", "time.AfterFunc", "time.After(", "time.Sleep"}
var files int
for _, root := range []string{"cmd", "internal"} {
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return err
}
files++
src, err := os.ReadFile(path)
if err != nil {
return err
}
for _, f := range forbidden {
if strings.Contains(string(src), f) {
t.Errorf("%s holds %s: the checker serves no request subject and holds no timer", path, f)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
if files < 1 {
t.Fatalf("read %d source files, want the program's", files)
}
}