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
213 lines
7.6 KiB
Go
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)
|
|
}
|
|
}
|