Files
mesh-delivery/modules/mesh-delivery-watchman/manifest_test.go
T
jschoubben 72d8e2155f DLV-M5: the watchman follows the assigner's wants and the indexer's modules by their read tools' first views, read once answered
ADR 0615 §1: the watchman invokes assigner_read_wants and indexer_read_modules, consumes their views
families and never reads either bucket; it keeps first-views and writes an unanswered first view as a
finding of its own. The lead's ruling: an owner that is down never holds the start; each state is read
on a goroutine once its first view is answered, and what it holds (seeded pulses, stuck keys, the
findings it is the source of) is reconciled then (watch.Awaits, ReadLate), never dropped at the start's
end. Tools watchman_first_views and watchman_first_view_again.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:06:04 +01:00

213 lines
7.6 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 (ParseManifest), an internal package of another
// repository, which this module cannot import. These tests therefore decode the manifest strictly into the fields its
// Manifest type names (DisallowUnknownFields, as its parseManifest does) and check the rules the issue names.
// **The merge gate's module check is what proves the controller takes this manifest (1135/AC6); these tests hold the
// rules so a unit run catches a refusal early.**
type state struct {
Name string `json:"name"`
History int `json:"history,omitempty"`
KeyTTL bool `json:"key-ttl,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"`
Context *struct {
Repository string `json:"repository"`
} `json:"context"`
}
type claim struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
}
type manifest struct {
Module string `json:"module"`
Claims []claim `json:"claims"`
Tools []string `json:"tools"`
Moved bool `json:"moved"`
Version string `json:"version"`
Slug string `json:"slug"`
Reads []string `json:"reads"`
Invokes []string `json:"invokes"`
Consumes []string `json:"consumes"`
State []state `json:"state"`
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 TestManifestStateAndReads(t *testing.T) { // 1135/AC6
m, top := load(t)
if m.Module != "mesh-delivery-watchman" || m.Moved || m.Slug != "watchman" {
t.Errorf("module %q moved %v (it has no template, so it is not moved) slug %q", m.Module, m.Moved, m.Slug)
}
want := []state{{Name: "presence", History: 20}, {Name: "pulse", KeyTTL: true}, {Name: "stuck", History: 20}, {Name: "findings", History: 5}, {Name: "first-views", History: 1}}
if !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
reads := slices.Clone(m.Reads)
slices.Sort(reads)
if !reflect.DeepEqual(reads, []string{"mesh-build-go.steps", "mesh-build-image.steps", "mesh-build-node.steps",
"mesh-delivery-builder.builds", "mesh-host.health", "mesh-provisioner.provisions", "node-engine.have"}) {
t.Errorf("reads = %v", reads)
}
// DLV-M5 (ADR 0615 §1): the assigner's wants and the indexer's modules are their owners' states, asked by their read
// tools and followed by their views families, never read.
for _, r := range m.Reads {
if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") {
t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r)
}
}
if want := []string{"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_modules"}; !reflect.DeepEqual(m.Invokes, want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
if want := []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.views.modules"}; !reflect.DeepEqual(m.Consumes, want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
// 1952/WM1: the watchman holds the seat mesh-watchdogs at mesh scope, its first seat (novox/hq ADR 0564 §1, §4).
if !reflect.DeepEqual(m.Claims, []claim{{Name: "mesh-watchdogs", Scope: "mesh", Serves: []string{"signals", "stuck", "presence", "dead-letters", "resync"}}}) {
t.Errorf("claims = %+v", m.Claims)
}
if !reflect.DeepEqual(m.Tools, []string{"watchman_findings", "watchman_first_views", "watchman_first_view_again"}) {
t.Errorf("tools = %v", m.Tools)
}
for _, k := range []string{"emits", "context", "items", "provides"} {
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the watchman has none", k)
}
}
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-watchman" {
t.Errorf("secrets-owner = %q", m.SecretsOwner)
}
}
// The manifest is whole in its repository: the program, and the tools bundle the runtime loads (1952/WM1).
func manifestIsWholeInItsRepository(t *testing.T, m manifest) { // 1135/AC6
if len(m.Build.Artifacts) != 2 {
t.Fatalf("artifacts = %+v, want the program and the tools bundle", m.Build.Artifacts)
}
for i, name := range []string{"program", "tools"} {
a := m.Build.Artifacts[i]
if a.Name != name || a.Kind != "bundle" || a.Language != "go" || a.System != "arch" || a.Context != nil {
t.Errorf("artifact = %+v", a)
}
if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
t.Errorf("the artifact builds from %q, which is not a directory of this module", a.From)
}
if !strings.HasSuffix(a.From, a.Binary) || (name == "tools") != reflect.DeepEqual(a.Loads, []string{a.Binary}) {
t.Errorf("binary %q from %q loads %v", a.Binary, a.From, a.Loads)
}
}
}
func TestManifestResources(t *testing.T) { // 1135/AC6
m, _ := load(t)
manifestIsWholeInItsRepository(t, m)
byType := map[string][]map[string]any{}
for _, r := range m.Resources {
ty, _ := r["type"].(string)
byType[ty] = append(byType[ty], r)
}
for _, ty := range []string{"user", "directory", "process"} {
if len(byType[ty]) == 0 {
t.Fatalf("no %s resource", ty)
}
}
user := byType["user"][0]["name"]
if user != "mesh-delivery-watchman" {
t.Errorf("user = %v", user)
}
p := byType["process"][0]
if p["user"] != user || p["artifact"] != "program" {
t.Errorf("process = %v", p)
}
env, _ := p["env"].(map[string]any)
if env["MESH_BROKER_FILE"] != "${dir:state}/broker" {
t.Errorf("the process reads %v", env["MESH_BROKER_FILE"])
}
if h, _ := p["health"].(map[string]any); h["kind"] != "unit" {
t.Errorf("health = %v", p["health"])
}
d := byType["directory"][0]
if d["id"] != "state" || d["mode"] != "0700" || d["owner"] != user {
t.Errorf("state directory = %v", d)
}
}
func TestNoTickerTimerOrSleep(t *testing.T) { // 1135/AC7
forbidden := []string{"time.Tick", "time.NewTicker", "time.Sleep", "time.After", "time.NewTimer", "time.AfterFunc"}
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 program holds no timer", path, f)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
if files < 3 {
t.Fatalf("read %d source files, want the program's", files)
}
}