Files
mesh-delivery/modules/mesh-delivery-view/manifest_test.go
T
jschoubben 952dedd0ed Join item 2: the readers follow through mesh-sdk's views.ProcessReader; sdk/firstview is gone
mesh-delivery's sdk/firstview moved whole into mesh-sdk go/views (J3): the builder, checker, watchman, teller and view
follow each owner's state with views.ProcessReader.Follow(owner, state, tool, stream), naming the owner's read tool as
their invokes name it and its views family as the owner's manifest's `streams` names it ("indexer-views",
"assigner-views"); a refused Follow stops the start. Their beds serve the owners with viewstest.Serve and grant with
viewstest.Grants by the same names. The view's tools ask the owners' read tools by their literal names. The assigner
names its own views family and the indexer's itself (views.AssignerStream and IndexerStream are gone).
Every views.StatusTools call takes handacts.Recorder{Ask: stdio.Ask}: a first view asked again by hand is a hand act,
recorded through seat:mesh-hand-acts.hand-act, which the builder's, checker's, watchman's, teller's and view's
manifests now invoke (a row in each manifest test, each failed first).
Row (A2j's written-time switch): TestRunNeedsItsCredential "a want stood since the assigner's bus wrote it": the
watchman's want time (createdOr) is the time the owner's bus wrote the key, through ProcessReader's first view (failed
with createdOr ignoring the entry's time). sdk/go.mod: nats.go is indirect now.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:10:36 +01:00

343 lines
15 KiB
Go

package manifest
import (
"bytes"
"encoding/json"
"io/fs"
"os"
"path/filepath"
"reflect"
"regexp"
"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 the
// controller's Manifest, Build and Artifact types name (internal/catalogue/manifest.go:475, :602, :659, :667, :737,
// :809, :821, :892, :1010, :1065-:1090), as its parseManifest does (DisallowUnknownFields, manifest.go:1744), and check
// the rules the issue names, and two rules of the controller's module check (a module with templates packs a files
// archive; a module contributing an item is moved). **The merge gate's module check is what proves the controller
// takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.**
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 item struct {
Collection string `json:"collection"`
Template string `json:"template"`
}
type state struct {
Name string `json:"name"`
History int `json:"history,omitempty"`
}
type manifest struct {
State []state `json:"state"`
Module string `json:"module"`
Moved bool `json:"moved"`
Version string `json:"version"`
Slug string `json:"slug"`
Reads []string `json:"reads"`
Consumes []string `json:"consumes"`
Invokes []string `json:"invokes"`
Uses []string `json:"uses"`
Tools []string `json:"tools"`
Emits []string `json:"emits"`
OwnSecrets map[string]string `json:"own-secrets"`
SecretsOwner string `json:"secrets-owner"`
Resources []map[string]any `json:"resources"`
Items []item `json:"items"`
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 TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
m, top := load(t)
if m.Module != "mesh-delivery-view" {
t.Errorf("module = %q", m.Module)
}
want := []string{"mesh-build-go.pauses", "mesh-build-go.steps", "mesh-build-image.pauses", "mesh-build-image.steps", "mesh-build-node.pauses", "mesh-build-node.steps", "mesh-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-gate.asks", "mesh-delivery-gate.source", "mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-engine.have"} // no reads of the indexer's or the assigner's states (ADR 0615 §1, DLV-M4); view_tiers reads the module graph and the provisioner's keys (ADR 0603 §6); view_status reads the keeper's conditions and the healers' attempts (1952/V1); build_usage reads the build modules' steps and pauses, mesh-config's capacities and the checker's tries; 1168/AC1, 1241/AC4, 1345/AC3, 1297/B8, B9
got := slices.Clone(m.Reads)
slices.Sort(got)
if !reflect.DeepEqual(got, want) {
t.Errorf("reads = %v, want exactly %v", got, want)
}
for _, k := range []string{"provides", "claims"} {
if _, ok := top[k]; ok {
t.Errorf("the manifest declares %q: the view writes only its answers", k)
}
}
// The indexer's and the assigner's states are asked of their owners' read tools (design 25 § 12.2), the six the process
// follows then heard on their views families (ADR 0615 §1, §5; DLV-M4).
if want := []string{"mesh-delivery-assigner.assigner_read_asks", "mesh-delivery-assigner.assigner_read_differences", "mesh-delivery-assigner.assigner_read_placement",
"mesh-delivery-assigner.assigner_read_wants", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_config_candidates",
"mesh-delivery-indexer.indexer_read_graph", "mesh-delivery-indexer.indexer_read_heard", "mesh-delivery-indexer.indexer_read_modules",
"mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_taken",
"seat:mesh-hand-acts.hand-act", // the join: its first view asked again by hand is a hand act (views.StatusTools)
"seat:node-engine.manifest-reads"}; !reflect.DeepEqual(m.Invokes, want) { // ADR 0618 §3: node_manifest_reads asks the node-engine seat's verb of every node
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
if want := []string{"mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.wants",
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.taken"}; !reflect.DeepEqual(m.Consumes, want) {
t.Errorf("consumes = %v, want %v", m.Consumes, want)
}
// The one event the view says: a source revision is delivered (1906/D9, ADR 0539 §17), derived by it from the wants and the haves.
if !reflect.DeepEqual(m.Emits, []string{"delivered", "moving", "settled", "stuck"}) {
t.Errorf("emits = %v, want exactly delivered, moving, settled and stuck (1906/D9, D10)", m.Emits)
}
// The view uses no build seat: it reads their workers by the broker's named grant and publishes no ask (ADR 0525 §2).
if len(m.Uses) != 0 {
t.Errorf("uses = %v, want none", m.Uses)
}
// The states the view writes: the operator's answers to the asks that need no proof (ADR 0528 clause 6, 1297/B9), which the
// teller reads, and how far each repository reached (ADR 0556 clause 2, issue 1280), which the gate reads.
// Its first views of the owners' states and the finding of one unanswered past its bound (ADR 0615 §6).
if want := []state{{Name: "answers", History: 5}, {Name: "reach", History: 1}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 {
t.Errorf("own-secrets = %v, want exactly broker (or no credential is issued)", m.OwnSecrets)
}
if m.SecretsOwner != "mesh-delivery-view" {
t.Errorf("secrets-owner = %q", m.SecretsOwner)
}
// The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer
// itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads.
if want := []string{"view_status", "indexer_module", "indexer_modules", "indexer_candidate", "indexer_candidates", "indexer_predefined", "indexer_config_candidates", "indexer_packages", "builder_build", "builder_builds", "builder_history", "assigner_wants", "assigner_want", "assigner_differences", "node_haves", "node_trusts", "build_usage", "view_reach", "indexer_module_history", "indexer_heard", "view_tiers", "view_first_views", "view_first_view_again", "node_manifest_reads"}; !reflect.DeepEqual(m.Tools, want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
for _, a := range m.Build.Artifacts {
// A bundle compiled to a binary is one executable: what the runtime loads is that binary's name, or the controller's
// module check refuses the manifest (mesh-sdk catalogue build.go, ADR 0193).
if a.Name == "tools" && !reflect.DeepEqual(a.Loads, []string{a.Binary}) {
t.Errorf("the tools artifact loads %v, want its binary %q", a.Loads, a.Binary)
}
}
}
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
m, _ := load(t)
if len(m.Build.Artifacts) != 4 {
t.Fatalf("artifacts = %+v", m.Build.Artifacts)
}
for _, a := range m.Build.Artifacts {
if a.Name == "files" {
continue
}
if a.Context != nil {
t.Errorf("artifact %s names a context of another repository", a.Name)
}
if a.Kind != "bundle" || a.Language != "go" || a.System != "arch" {
t.Errorf("artifact %s = %+v, want a go bundle for arch", a.Name, a)
}
if st, err := os.Stat(a.From); err != nil || !st.IsDir() {
t.Errorf("artifact %s builds from %q, which is not a directory of this module", a.Name, a.From)
}
if a.Binary == "" || !strings.HasSuffix(a.From, a.Binary) {
t.Errorf("artifact %s binary %q does not match %q", a.Name, a.Binary, a.From)
}
}
names := map[string]string{}
for _, a := range m.Build.Artifacts {
names[a.Name] = a.Binary
}
if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" || names["tools"] != "mesh-delivery-view-tools" {
t.Errorf("artifacts = %v", names)
}
}
func TestManifestResources(t *testing.T) { // 1134/AC7
m, _ := load(t)
byType := map[string][]map[string]any{}
for _, r := range m.Resources {
byType[r["type"].(string)] = append(byType[r["type"].(string)], r)
}
for _, ty := range []string{"user", "directory", "process", "archive"} {
if len(byType[ty]) == 0 {
t.Fatalf("no %s resource", ty)
}
}
user := byType["user"][0]["name"]
if user != "mesh-delivery-view" {
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" || m.OwnSecrets["broker"] != "${dir:state}/broker" {
t.Errorf("the process reads %v, the own-secret is written at %v", env["MESH_BROKER_FILE"], m.OwnSecrets["broker"])
}
if h, _ := p["health"].(map[string]any); h["kind"] != "unit" {
t.Errorf("health = %v", p["health"])
}
var state, run bool
for _, d := range byType["directory"] {
if d["id"] == "state" && d["mode"] == "0700" && d["owner"] == user {
state = true
}
if d["path"] == "/var/lib/mesh-delivery-view-cli" && d["mode"] == "0755" && d["owner"] == user {
run = true
}
}
if !state || !run {
t.Errorf("state directory 0700 owned by the user: %v; run directory for the socket 0755: %v", state, run)
}
a := byType["archive"][0]
if a["artifact"] != "cli" || a["path"] != "/usr/local/lib/mesh-delivery-view" {
t.Errorf("archive = %v, want the cli unpacked into its own directory", a)
}
}
func TestModuleCheckRules(t *testing.T) { // 1134/AC7
m, _ := load(t)
if len(m.Items) > 0 && !m.Moved {
t.Error("a module contributing an item is marked moved: true")
}
var files bool
for _, a := range m.Build.Artifacts {
files = files || (a.Name == "files" && a.Kind == "archive" && a.From == "files")
}
if len(m.Items) > 0 && !files {
t.Error("a module naming templates packs a files archive from files/")
}
}
func TestAliasItem(t *testing.T) { // 1137/AC1
m, _ := load(t)
if !reflect.DeepEqual(m.Items, []item{{Collection: "shell:zsh:normal", Template: "files/zsh-normal"}}) {
t.Fatalf("items = %+v", m.Items)
}
raw, err := os.ReadFile("files/zsh-normal")
if err != nil {
t.Fatal(err)
}
if string(raw) != "alias nox-delivery=mesh-delivery-cli\n" {
t.Fatalf("files/zsh-normal = %q, want the one line", raw)
}
}
// The controller refuses two modules declaring one path (internal/catalogue/resolve.go:1160-1168). mesh-cli (mesh-catalog
// modules/mesh-cli/module.json) declares /usr/local/bin whole, so this module may place only a file below a shared
// directory, never the directory itself.
func TestNoSharedDirectoryIsClaimed(t *testing.T) { // 1134/AC7
m, _ := load(t)
shared := map[string]bool{"/usr/local/bin": true, "/usr/local/lib": true, "/usr/bin": true, "/usr/local": true, "/usr/lib": true, "/etc": true, "/opt": true}
var binary bool
for _, r := range m.Resources {
p, _ := r["path"].(string)
if shared[strings.TrimRight(p, "/")] {
t.Errorf("resource %v declares the shared directory %s, which mesh-cli and others declare too", r["id"], p)
}
if p == "/usr/local/bin/mesh-delivery-cli" && r["type"] == "file" && r["template"] == "files/mesh-delivery-cli" {
binary = true
}
}
if !binary {
t.Error("the command is placed as the file /usr/local/bin/mesh-delivery-cli")
}
raw, err := os.ReadFile("files/mesh-delivery-cli")
if err != nil || !strings.Contains(string(raw), "exec /usr/local/lib/mesh-delivery-view/mesh-delivery-cli") {
t.Errorf("files/mesh-delivery-cli = %q, %v", raw, err)
}
}
// The node-engine never places a module-owned directory below /etc, /usr, /run or /var/run: they are root's
// (mesh-host internal/apply/placement_guard.go:80, rootsOnly; novox/hq issue 339).
func TestNoDirectoryBelowRootsTrees(t *testing.T) { // 1134/AC10
m, _ := load(t)
for _, r := range m.Resources {
if r["type"] != "directory" {
continue
}
p, _ := r["path"].(string)
for _, root := range []string{"/etc", "/usr", "/run", "/var/run"} {
if p == root || strings.HasPrefix(p, root+"/") {
t.Errorf("directory %v at %s is below %s, which is root's", r["id"], p, root)
}
}
}
var socketDir bool
for _, r := range m.Resources {
if r["path"] == "/var/lib/mesh-delivery-view-cli" && r["type"] == "directory" && r["mode"] == "0755" && r["owner"] == "mesh-delivery-view" {
socketDir = true
}
}
if !socketDir {
t.Error("the socket's directory is /var/lib/mesh-delivery-view-cli, 0755, owned by mesh-delivery-view")
}
}
// TestEveryStateTheCommandsReadIsIssued holds every literal stdio.State name in cmd/ to what the mesh issues this
// module: a state of its own by its bare name, another module's by <module>.<name> as `reads` lists it. The tools read
// their own reach as "mesh-delivery-view.reach", which the mesh never issued, and view_reach failed on the machines
// (issue 1280, note 13).
func TestEveryStateTheCommandsReadIsIssued(t *testing.T) {
m, _ := load(t)
issued := map[string]bool{}
for _, s := range m.State {
issued[s.Name] = true
}
for _, r := range m.Reads {
issued[r] = true
}
literal := regexp.MustCompile(`stdio\.State\("([^"]+)"\)`)
seen := 0
err := filepath.WalkDir("cmd", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") {
return err
}
src, err := os.ReadFile(path)
if err != nil {
return err
}
for _, match := range literal.FindAllSubmatch(src, -1) {
seen++
if name := string(match[1]); !issued[name] {
t.Errorf("%s reads the state %q, which the mesh does not issue this module (its own by bare name, another's as `reads` lists it)", path, name)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
if seen == 0 {
t.Fatal("no stdio.State literal found under cmd/: the scan reads nothing")
}
}