Files
mesh-delivery/modules/mesh-delivery-view/manifest_test.go
T
jschoubben 0de534c692 Names N3 (view): the status view names its own buckets from its credential's module
Its states answers, reach and waits (viewstate WaitsBucket, ReachBucket, ViewAnswersBucket), its
first views and its marks were named from the constant ViewModule; they are now
`<cred.Module>_<state>` (viewstate.BucketsOf), and the model is made for its module.

Builds: design 47 G A-296

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 19:14:55 +01:00

513 lines
25 KiB
Go

package manifest
import (
"bytes"
"encoding/json"
"io/fs"
"mesh-delivery-view/internal/view"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// 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"`
Paths []string `json:"paths"`
Series string `json:"series"`
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 stream struct {
Name string `json:"name"`
Events []string `json:"events"`
MaxAgeDays int `json:"max-age-days"`
MaxMiB int `json:"max-mib"`
}
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 []invoke `json:"invokes"`
Uses []string `json:"uses"`
Tools []string `json:"tools"`
Emits []string `json:"emits"`
Streams []stream `json:"streams"`
OwnSecrets map[string]string `json:"own-secrets"`
SecretsOwner string `json:"secrets-owner"`
Resources []map[string]any `json:"resources"`
Requires []require `json:"requires"`
Items []item `json:"items"`
Build struct {
Artifacts []artifact `json:"artifacts"`
} `json:"build"`
}
// invoke is an entry of `invokes`: a verb, or in its long form {verb, fields}, the fields of the verb's answer the view
// relies on (design 47 G §558).
type invoke struct {
Verb string `json:"verb"`
Fields []string `json:"fields,omitempty"`
}
func (i *invoke) UnmarshalJSON(raw []byte) error {
if err := json.Unmarshal(raw, &i.Verb); err == nil {
return nil
}
type long invoke
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
return dec.Decode((*long)(i))
}
// verbs are the verbs of the invokes, whichever their form.
func verbs(in []invoke) []string {
out := make([]string, 0, len(in))
for _, i := range in {
out = append(out, i.Verb)
}
return out
}
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-conditions.conditions", "mesh-config.resolved", "mesh-delivery-builder.build-log", "mesh-delivery-builder.builds", "mesh-delivery-checker.tries", "mesh-delivery-watchman.presence", "mesh-delivery-watchman.stuck", "mesh-healers.attempts", "mesh-provisioner.provisions", "mesh-vault.signers", "node-build-go.pauses", "node-build-go.steps", "node-build-image.pauses", "node-build-image.steps", "node-build-node.pauses", "node-build-node.steps", "node-engine.have"} // the build seats' states by seat, never a build module (design 47 G A-82); no read of the gate's source: each watched branch's is its verb source's (§512, A-173); // 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 := []invoke{{Verb: "mesh-delivery-assigner.assigner_read_asks"}, {Verb: "mesh-delivery-assigner.assigner_read_differences"}, {Verb: "mesh-delivery-assigner.assigner_read_assignments"},
{Verb: "mesh-delivery-assigner.assigner_read_wants"}, {Verb: "mesh-delivery-indexer.indexer_read_candidates"}, {Verb: "mesh-delivery-indexer.indexer_read_config_candidates"},
{Verb: "mesh-delivery-indexer.indexer_read_graph"}, {Verb: "mesh-delivery-indexer.indexer_read_heard"}, {Verb: "mesh-delivery-indexer.indexer_pending"},
{Verb: "mesh-delivery-indexer.indexer_read_packages"}, {Verb: "mesh-delivery-indexer.indexer_read_predefined"}, {Verb: "mesh-delivery-indexer.indexer_read_taken"},
{Verb: "mesh-delivery-teller.teller_read_answers"}, // A-175: the teller's answers by their views family
{Verb: "seat:mesh-catalog.revision", Fields: []string{"none-yet"}},
{Verb: "seat:mesh-catalog.registrations"}, // §614: the watched branches of the registrations, then registration-changed
{Verb: "seat:mesh-catalog.entry", Fields: []string{"manifest-digest"}}, // §558: the module's value is the entry's manifest-digest (§506)
{Verb: "seat:mesh-delivery.source", Fields: []string{"branches", "group", "repositories", "revision"}}, // §507, §614, A-173: the branches before the first registrations, and each watched branch's source (§558: fields G adds) // ADR 0663: the modules are the catalog half's, read through the seat's verb revision, never the indexer's
{Verb: "seat:mesh-catalog.seat-definitions"}, // design 47 G A-82, §204: the build seats are read from the seat list
{Verb: "seat:mesh-broker.show"}, // A-274: the binding of its finding-events
{Verb: "seat:mesh-hand-acts.hand-act"}, // the join: its first view asked again by hand is a hand act (views.StatusTools)
{Verb: "seat:mesh-module-assigner.assignments"}, // A-82: the holder of each build seat
{Verb: "seat:mesh-wants.revision"}, // §512, A-242: view_tiers compares kept assignment revisions
{Verb: "seat:mesh-wants.difference"}, // §512, A-242: the tiers and each change's after are the assigner's
{Verb: "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; 1585/T24: view_tiers asks indexer_pending, a verb edge (ADR 0614 §2)
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
// design 47 G §387: the keeper's ask for the findings is heard, the finding stream declared (§383), and §368: version is MAJOR.MINOR.
if len(m.Streams) != 0 {
t.Errorf("streams = %+v: the finding stream is a require now", m.Streams)
}
if want := findingRequires(1); !reflect.DeepEqual(m.Requires, want) {
t.Errorf("requires = %+v, want %+v", m.Requires, want)
}
if m.Version != "0.1" {
t.Errorf("version = %q, want 0.1", m.Version)
}
if want := []string{"mesh-conditions.findings-asked", "mesh-delivery-assigner.views.asks", "mesh-delivery-assigner.views.assignments", "seat:mesh-module-assigner.views.seat-holders", "mesh-delivery-assigner.views.wants",
"mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.taken", "mesh-delivery-teller.views.answers",
"mesh-wants.wait-held", "mesh-wants.wait-ended", "node-engine.wait-held", "node-engine.wait-ended", "mesh-pipeline-runner.wait-held", "mesh-pipeline-runner.wait-ended",
"mesh-grants.wait-held", "mesh-grants.wait-ended", "mesh-delivery-builder.wait-held", "mesh-delivery-builder.wait-ended",
"mesh-broker.recreated", "mesh-broker.dropped"}; !reflect.DeepEqual(m.Consumes, want) { // design 47 G §512, A-242: every part's wait-held and wait-ended
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", "reached", "moving", "settled", "stuck", "views.answers"}) {
t.Errorf("emits = %v, want exactly delivered, reached (ADR 0596 §2), moving, settled and stuck (1906/D9, D10), and views.answers (A-178)", 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: "waits", History: 1}, {Name: "first-views", History: 1}, {Name: "view-marks", 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", "view_seat_list", "view_seat_list_again", "view_read_answers"}; !reflect.DeepEqual(m.Tools, want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
// D-11: the holders and buckets the view names are its manifest's (no contract names a holder, novox/hq ADR 0634 §1): a
// state it reads directly is in `reads`, one it follows is a family it consumes, and the owners' tools it asks are invoked.
t.Run("the names are the manifest's", func(t *testing.T) {
direct := map[string]string{view.PresenceBucket: view.WatchmanModule + ".presence", view.StuckBucket: view.WatchmanModule + ".stuck", view.HaveBucket: view.EngineModule + ".have"}
for bucket, read := range direct {
if strings.ReplaceAll(read, ".", "_") != bucket {
t.Errorf("the bucket %s is not the state %s", bucket, read)
}
if !slices.Contains(m.Reads, read) {
t.Errorf("the view reads %s, its manifest's reads are %v", read, m.Reads)
}
}
if !slices.Contains(m.Reads, view.ProvisionsRead) {
t.Errorf("the view reads %s, its manifest's reads are %v", view.ProvisionsRead, m.Reads)
}
if !slices.Contains(m.Reads, view.SignersRead) {
t.Errorf("the view reads %s, its manifest's reads are %v", view.SignersRead, m.Reads)
}
followed := map[string]string{view.CandidatesBucket: view.IndexerModule + ".views.candidates",
view.TakenBucket: view.IndexerModule + ".views.taken", view.AssignmentsBucket: view.AssignerModule + ".views.assignments",
view.AssignerWantsBucket: view.AssignerModule + ".views.wants", view.AssignerAsksBucket: view.AssignerModule + ".views.asks",
view.AnswersBucket: view.TellerModule + ".views.answers"}
for bucket, family := range followed {
if module, state, _ := strings.Cut(family, ".views."); bucket != module+"_"+state {
t.Errorf("the bucket %s is not the state %s of %s", bucket, state, module)
}
if !slices.Contains(m.Consumes, family) {
t.Errorf("the view follows %s, its manifest consumes %v", family, m.Consumes)
}
}
for _, tool := range []string{meshcatalogclient.RevisionVerb, meshcatalogclient.RegistrationsVerb, meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb, view.IndexerModule + ".indexer_read_candidates", view.IndexerModule + ".indexer_pending", view.AssignerModule + ".assigner_read_wants", view.TellerModule + ".teller_read_answers"} {
if !slices.Contains(verbs(m.Invokes), tool) {
t.Errorf("the view asks %s, its manifest invokes %v", tool, m.Invokes)
}
}
// The gate's asks go with the Approve per merge, and the teller's answers are followed by their views family, never read
// (design 47 G §431, §512, A-175).
for _, read := range []string{view.GateModule + ".asks", view.TellerModule + ".answers"} {
if slices.Contains(m.Reads, read) {
t.Errorf("the manifest reads %s (design 47 G §512)", read)
}
}
// The modules are the catalog half's: the seat's verb is invoked, its event consumed, and the indexer's state modules is neither.
if !slices.Contains(m.Consumes, view.CatalogSeat+"."+meshcatalog.EventCatalogRevision) {
t.Errorf("the view hears the seat's %s, its manifest consumes %v", meshcatalog.EventCatalogRevision, m.Consumes)
}
for _, name := range append(verbs(m.Invokes), m.Consumes...) {
if strings.Contains(name, "indexer_read_modules") || strings.Contains(name, "views.modules") {
t.Errorf("the manifest names %s: the modules are the catalog half's (ADR 0663)", name)
}
}
if viewstate.ViewModule != m.Module {
t.Errorf("the view names itself %s, its manifest %s", viewstate.ViewModule, m.Module)
}
// A-296: its own buckets are named from its credential's module, each a state its manifest keeps.
var own []string
for _, s := range m.State {
own = append(own, "a-view_"+s.Name)
}
b := viewstate.BucketsOf("a-view")
for _, want := range []string{b.Answers, b.Reach, b.Waits} {
if !slices.Contains(own, want) {
t.Errorf("the view keeps %s, its manifest's states are %v", want, own)
}
}
// The packages key is the seat's key of its state packages for a module (<module>), as the indexer writes it: the
// escaped path, with no dot, so a reader never takes it for a <package>.<series> key.
const path = "git.novox.be/novox/mesh-sdk/go"
key, err := view.PackagesKey(path)
if want := meshcatalog.PackagesByModuleKey(keys.EscapeName(path)); err != nil || key != want || strings.Contains(key, ".") {
t.Errorf("the packages key of %s is %q (%v), the seat's is %q", path, key, err, want)
}
if module, ok := meshcatalog.ParsePackagesByModuleKey(key); !ok || module != keys.EscapeName(path) {
t.Errorf("the seat does not read %q as a module key: %q %v", key, module, ok)
}
})
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)
// Issue 1966's bootstrap: the package artifact viewstate is out of the manifest until the new builder runs; the
// follow-up puts the count back at 5.
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.Name == "viewstate" { // ADR 0634 §1: the module's own package, published for the gate
if a.Kind != "package" || a.Language != "go" || len(a.Paths) != 1 || a.Paths[0] != "viewstate" || a.Series != "0.1" {
t.Errorf("package artifact = %+v", a)
}
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/assignments_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")
}
}
// require is one entry of the manifest's requires: a provision the module asks for by a template (ADR 0595), which the
// controller's reader takes.
type require struct {
Name string `json:"name"`
Storage string `json:"storage"`
Template string `json:"template"`
Options map[string]any `json:"options"`
}
// findingRequires is the findings state and the finding stream a module asks for by their templates (design 47 G
// §383, §387; A-30): 7 days and 16 MiB, the findings state keeping the given history.
func findingRequires(history float64) []require {
return []require{
{Name: "findings", Storage: "key-value", Template: "state", Options: map[string]any{"history": history}},
{Name: "finding-events", Storage: "stream", Template: "events", Options: map[string]any{
"events": []any{"finding-written.*", "finding-cleared.*", "findings-stated.*"}, "max-age-days": float64(7), "max-mib": float64(16)}},
}
}