package manifest import ( "bytes" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "reflect" "slices" "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521, // Invokes :590, Streams :613, Reads :659; StreamDeclaration, streams.go), an internal package of another repository. // These tests decode the manifest strictly into the fields those types name (DisallowUnknownFields) and hold the // rules of issue 1607 (ADR 0453 §4, ADR 0478). The merge gate's module check proves the controller takes the manifest. type state struct { Name string `json:"name"` History int `json:"history,omitempty"` KeyTTL bool `json:"key-ttl,omitempty"` MaxMiB int `json:"max-mib,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"` Paths []string `json:"paths"` Series string `json:"series"` } type manifest struct { Module string `json:"module"` Version string `json:"version"` Slug string `json:"slug"` Claims []struct { Name string `json:"name"` Scope string `json:"scope"` Serves []string `json:"serves"` } `json:"claims"` Tools []string `json:"tools"` Emits []string `json:"emits"` Consumes []string `json:"consumes"` State []state `json:"state"` Streams []struct { Name string `json:"name"` Events []string `json:"events"` MaxAgeDays int `json:"max-age-days"` MaxMiB int `json:"max-mib"` LastPerSubject bool `json:"last-per-subject"` } `json:"streams"` Logs []struct { Name string `json:"name"` MaxAgeDays int `json:"max-age-days"` MaxMiB int `json:"max-mib"` } `json:"logs"` Reads []string `json:"reads"` Uses []string `json:"uses"` Invokes []string `json:"invokes"` Config map[string]struct { Type string `json:"type"` Description string `json:"description"` Pattern string `json:"pattern"` Minimum float64 `json:"minimum"` Maximum float64 `json:"maximum"` Trusted bool `json:"trusted"` Default any `json:"default"` } `json:"config"` 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 sorted(s []string) []string { s = slices.Clone(s) slices.Sort(s) return s } func TestManifestDeclarations(t *testing.T) { // 1607/AC1 m, top := load(t) if m.Module != "mesh-delivery-builder" { t.Errorf("module = %q", m.Module) } if want := []state{{Name: "builds", History: 5}, {Name: "engine-reads", History: 1}, {Name: "findings", History: 5}, {Name: "floor-waits", History: 1}, {Name: "hearing", History: 1}, {Name: "instances", History: 1}, {Name: "old-form", History: 1}, {Name: "bounds", History: 1, KeyTTL: true}, {Name: "queues", History: 1}, {Name: "toolchains-heard", History: 1}, {Name: "waits", History: 1, MaxMiB: 4}, {Name: "reserved", History: 1}, {Name: "publish-queue", History: 1, MaxMiB: 4}, {Name: "acted", History: 1, MaxMiB: 1}, {Name: "asked", History: 1}, {Name: "reasks", History: 1}, {Name: "versions", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) { // builds.BuilderBuildsHistory t.Errorf("state = %+v, want %+v", m.State, want) } // 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and // holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5). Whether a // node's machine is heard it asks the watchdogs seat's presence (issue 1966, ADR 0596 §2, ADR 0668 §3). // Each build seat's steps are read in seat form, its holder found where it is read (A-116, A-128). if want := []string{"seat:mesh-module-assigner.assignments", "seat:node-build-go.steps", "seat:node-build-image.steps", "seat:node-build-node.steps"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } if want := []string{"node-build-go", "node-build-image", "node-build-node", "operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) { t.Errorf("uses = %v, want the three build seats and operator-channel (design 47 G §568: the ask after three event builds)", m.Uses) } // 1937/AC3: the builder asks the controller whether the artifact store still holds a done key's blobs (held), and no // other tool; it holds no capacity. // 1955/L7 (ADR 0571 §2): the seat mesh-artifact-sweep's held, the controller's `held` is no longer asked (issue 1955). // DLV-M1 (ADR 0615 §1): the assigner's assignments are asked by their read tool and followed by their views family, never // read; the catalog half (the modules, their manifests, their packages and the package names a package is numbered under) is // read through the seat mesh-catalog's branch follower (design 47 G §348, §614), and the indexer's states not at all. for _, r := range m.Reads { // The toolchain gate follows the assigner's assignments in the bucket of the seat's holder, so it declares that one read by the // seat (A-116, A-119); no module is named and everything else of the assigner's is asked by tools. 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) } } // Design 51 *The floor*: the floor's three sources, the registry seats' versions, git.tags and indexer_requirements. // The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3). if want := []string{"seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", "seat:git.tags", "seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove", "seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions", // A-231: the build asks on each build seat's queue delivery (accept.delivery.). "seat:node-build-go.accept.delivery", "seat:node-build-node.accept.delivery", "seat:node-build-image.accept.delivery"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The builder hears the build seats' toolchain-reported and the node-engine seat's running (design 47 G §492, §546) and reads no // build module's toolchains; and by the seat mesh-artifact-sweep's artifacts-gone, a blob the sweep found gone (1937/AC4, 1955/L7), the // controller's no longer (1126/B9, ADR 0590). // Design 51 *The floor*: the indexer's manifest-accepted and the condition keeper's condition-cleared wake a silent floor. // DLV-B4: the four registry seats' outcomes, each a seat event `.` on the shared events stream. // DLV-M1: the indexer's and the assigner's views families of the states it follows. if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed", "go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed", "mesh-artifact-sweep.artifacts-gone", "mesh-catalog.manifest-accepted", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-catalog.version-released", "mesh-catalog.versions-filled", "mesh-conditions.condition-cleared", "mesh-conditions.findings-asked", "seat:mesh-module-assigner.views.assignments", "seat:mesh-module-assigner.views.seat-holders", // A-298: the claimant's seat-holders view resolves the callers' seat entries "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "node-build-go.toolchain-reported", "node-build-node.toolchain-reported", "node-engine.manifest-reads-changed", "node-engine.running", "npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed", "oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed", "operator-channel.decided"}; !reflect.DeepEqual(sorted(m.Consumes), sorted(want)) { t.Errorf("consumes = %v, want %v", m.Consumes, want) } 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-builder" { t.Errorf("secrets-owner = %q", m.SecretsOwner) } // 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again. if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views", "builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_list", "builder_seat_list_again", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits"}; !reflect.DeepEqual(sorted(m.Tools), want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // Design 47 G §672: the toolchain waits' stream, kept 7 days within 16 MiB, and the bound's setting, 60 minutes. // Design 47 G A-30, §383: and its own findings' events, which its finding writer says. if len(m.Streams) != 3 || m.Streams[0].Name != "waits" || !reflect.DeepEqual(m.Streams[0].Events, []string{"wait-held.**", "wait-ended.**"}) || m.Streams[0].MaxAgeDays != 7 || m.Streams[0].MaxMiB != 16 || m.Streams[1].Name != builds.BuildsStream || !reflect.DeepEqual(m.Streams[1].Events, []string{builds.BuildsStreamEvents}) || m.Streams[1].MaxAgeDays != 7 || !m.Streams[1].LastPerSubject || m.Streams[2].Name != "mesh-delivery-builder-findings" || !reflect.DeepEqual(m.Streams[2].Events, []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}) { t.Errorf("streams = %+v, want waits with wait-held.** and wait-ended.**, 7 days, 16 MiB, the builds' stream (build.ended.**, 7 days, kept last per subject: design 47 G A-181) and the findings' stream", m.Streams) } if c, ok := m.Config["toolchain-wait-minutes"]; !ok || c.Default != float64(60) || len(m.Config) != 2 || m.Config["republish-max-versions"].Default != float64(50) { t.Errorf("config = %+v, want toolchain-wait-minutes of 60 and republish-max-versions of 50 (design 47 G §588, §633)", m.Config) } // Design 47 G §349: version.numbered is no longer said; old-form-closed on the family version, apart from the builds' ends. if want := []string{"build.asked-again", "version.old-form-closed"}; !reflect.DeepEqual(m.Emits, want) { t.Errorf("emits = %v, want %v", m.Emits, want) } // 1955/L7 (ADR 0571 §1): its module log, each build's start and end, kept 30 days in a cap that holds them; no bare stream. if len(m.Logs) != 1 || m.Logs[0].Name != builds.BuilderLog || m.Logs[0].MaxAgeDays != builds.BuilderLogDays || m.Logs[0].MaxMiB != builds.BuilderLogMiB { t.Errorf("logs = %+v, want %s kept %d days in %d MiB", m.Logs, builds.BuilderLog, builds.BuilderLogDays, builds.BuilderLogMiB) } // Design 47 G §52, §369: the seat mesh-builds, held once for the mesh, serving reserved. if len(m.Claims) != 1 || m.Claims[0].Name != "mesh-builds" || m.Claims[0].Scope != "mesh" || !reflect.DeepEqual(m.Claims[0].Serves, []string{"reserved"}) { t.Errorf("claims = %+v, want mesh-builds, scope mesh, serving reserved", m.Claims) } // A-115: the process answers the verb itself and says so in `serves` beside its process, with no `bus`; the bundle serves no tool named so. var resources []map[string]any if err := json.Unmarshal(top["resources"], &resources); err != nil { t.Fatal(err) } for _, r := range resources { if r["type"] == "process" && !reflect.DeepEqual(r["serves"], []any{"seat:mesh-builds.reserved"}) { t.Errorf("the process serves %v, want seat:mesh-builds.reserved (A-115)", r["serves"]) } } if slices.Contains(m.Tools, "reserved") { t.Error("the tools bundle serves reserved, which the process answers") } for _, k := range []string{"provides", "requires", "defines-seats", "seat", "seats", "bus"} { if _, ok := top[k]; ok { t.Errorf("the manifest declares %q: the builder has none", k) } } } func TestManifestProcess(t *testing.T) { // 1607/AC1 m, _ := load(t) var procs []map[string]any for _, r := range m.Resources { if r["type"] == "process" { procs = append(procs, r) } if r["type"] == "tool" || r["type"] == "seat" { t.Errorf("resource %v: the builder's tools are its bundle's, and it holds no seat", r) } } if len(procs) != 1 { t.Fatalf("%d process resources, want one", len(procs)) } p := procs[0] if p["artifact"] != "program" || !reflect.DeepEqual(p["run"], []any{"./mesh-delivery-builder"}) { t.Errorf("process = %v", p) } env, _ := p["env"].(map[string]any) if len(env) != 3 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_BUILDER_REPUBLISH_MAX_FILE"] != "${dir:state}/republish-max-versions" || env["MESH_BUILDER_TOOLCHAIN_WAIT_FILE"] != "${dir:state}/toolchain-wait-minutes" { t.Errorf("env = %v, want the broker file and the path of the toolchain wait's file, never the setting itself (design 47 G §672, A-71)", p["env"]) } for k, v := range env { if strings.Contains(fmt.Sprint(v), "${config:") { t.Errorf("env %s = %v: a setting reaches the process as a file, never its environment (A-71)", k, v) } } found := false for _, r := range m.Resources { if r["type"] == "file" && r["path"] == "${dir:state}/toolchain-wait-minutes" && r["content"] == "${config:toolchain-wait-minutes}" { found = true } } if !found { t.Errorf("no file resource with the content ${config:toolchain-wait-minutes}: A-71") } for _, r := range m.Resources { if r["id"] == "settings" { t.Errorf("a settings resource %v: the builder holds no setting since the capacity is the build modules' (ADR 0525 §5)", r) } } if _, bus := p["bus"]; bus { t.Errorf("the process declares a bus: it runs on its own broker credential") } // Issue 1966's bootstrap: the package artifact builds is out of the manifest until the new builder runs; the follow-up // puts it back with its assertion (ADR 0634 §1, ADR 0606 §4: three artifacts, the third the package builds). if len(m.Build.Artifacts) != 2 { t.Fatalf("artifacts = %+v, want the program and the tools", m.Build.Artifacts) } if a := m.Build.Artifacts[1]; a.Name != "tools" || a.Kind != "bundle" || a.Binary != "mesh-delivery-builder-tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-builder-tools"}) { t.Errorf("tools artifact = %+v", a) // 1945 } else if st, err := os.Stat(a.From); err != nil || !st.IsDir() { t.Errorf("the tools' source %s: %v", a.From, err) } a := m.Build.Artifacts[0] if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-builder" || !strings.HasSuffix(a.From, a.Binary) || a.Kind != "bundle" || a.Language != "go" { t.Errorf("artifact = %+v", a) } } // Design 47 G A-107 relaxes ADR 0437 §8 for a seat's verbs and its accepted kinds: the process answers them directly over the bus, // the verbs through mesh-sdk's conn.AnswerSeat (a call the guard below does not forbid), the accepted kind `step` through the one // named subscription. Any other request subject, and every ticker, timer and sleep, stays refused. func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1607/AC6 (ADR 0437 §8: no timer, a wait is a context's bound) forbidden := []string{"Subscribe(", "QueueSubscribe", "ChanSubscribe", "time.NewTicker", "time.Tick", "time.NewTimer", "time.AfterFunc", "time.After(", "time.Sleep"} 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 { // The one subscription is the seat mesh-builds' accepted kind `step`, the runner's build ask (design 47 G A-97, A-107). if f == "Subscribe(" && filepath.ToSlash(path) == "cmd/mesh-delivery-builder/step.go" { continue } if strings.Contains(string(src), f) { t.Errorf("%s holds %s: the builder serves no request subject but its seat's verbs and accepted kinds (A-107) and holds no timer", path, f) } } return nil }) if err != nil { t.Fatal(err) } } if files < 1 { t.Fatalf("read %d source files, want the program's", files) } }