Files
mesh-delivery/modules/mesh-delivery-view/manifest_test.go
T
jschoubben af96eeba86 A-274: the watchman, forge bridge, teller and view pass their finding-events binding to the finding writer
Each asks the seat mesh-broker's show for its finding-events entry (process.Binding, started without waiting), passes it
as the writer's Bindings and hears the seat's recreated and dropped; the watchman's one handler of the seats' events
hands those two to the binding, since a message has one taker. The teller, view and watchman manifests gain the invoke
seat:mesh-broker.show and consume mesh-broker.recreated and dropped. The forge bridge's and the watchman's whole tests
answer show and read the finding-written under the binding's subject.

Builds: design 47 G A-274, §529, §577, A-13

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 11:56:08 +01:00

501 lines
25 KiB
Go

package manifest
import (
"bytes"
"encoding/json"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"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/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", "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 || viewstate.ViewAnswersBucket != m.Module+"_answers" || viewstate.ReachBucket != m.Module+"_reach" {
t.Errorf("the view names itself %s (%s, %s), its manifest %s", viewstate.ViewModule, viewstate.ViewAnswersBucket, viewstate.ReachBucket, m.Module)
}
// 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)}},
}
}