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
240 lines
12 KiB
Go
240 lines
12 KiB
Go
package manifest
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
type manifest struct {
|
|
Module string `json:"module"`
|
|
Version string `json:"version"`
|
|
Slug string `json:"slug"`
|
|
Tools []string `json:"tools"`
|
|
Emits []string `json:"emits"`
|
|
Consumes []string `json:"consumes"`
|
|
State []state `json:"state"`
|
|
Logs []struct {
|
|
Name string `json:"name"`
|
|
MaxAgeDays int `json:"max-age-days"`
|
|
} `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"`
|
|
Default string `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: "queues", History: 1}, {Name: "reasks", History: 1}, {Name: "tag-ask-bounds", History: 1, KeyTTL: true}, {Name: "tag-asks", History: 1}, {Name: "versions", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) { // delivery.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).
|
|
if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains",
|
|
"mesh-delivery-gate.source", "mesh-delivery-indexer.packages"}; !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"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
|
t.Errorf("uses = %v, want the three build seats", 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 indexer's modules, manifests and candidates and the assigner's placement are 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.")) && r != "mesh-delivery-indexer.packages" {
|
|
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, bridge_tags and indexer_requirements.
|
|
if want := []string{"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
|
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
|
|
"seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-hand-acts.hand-act", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove",
|
|
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 {
|
|
t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want)
|
|
}
|
|
// The builder is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it
|
|
// starts; 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 `<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.published", "container-image-registry.refused", "container-image-registry.removed",
|
|
"go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
|
|
"mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
|
|
"mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.placement", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**",
|
|
"mesh-delivery-indexer.manifest-accepted", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
|
"node-engine.manifest-reads-changed",
|
|
"npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
|
|
"oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), 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_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_seat_queue", "builder_stuck", "builder_tag", "builder_tag_asks", "builder_untag", "builder_versions"}; !reflect.DeepEqual(sorted(m.Tools), want) {
|
|
t.Errorf("tools = %v, want %v", m.Tools, want)
|
|
}
|
|
// Design 51 *Events of the builder*: numbered and old-form-closed on the family version, apart from the builds' ends.
|
|
if want := []string{"build.asked-again", "version.numbered", "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; no bare stream.
|
|
if len(m.Logs) != 1 || m.Logs[0].Name != delivery.BuilderLog || m.Logs[0].MaxAgeDays != delivery.BuilderLogDays {
|
|
t.Errorf("logs = %+v, want %s kept %d days", m.Logs, delivery.BuilderLog, delivery.BuilderLogDays)
|
|
}
|
|
for _, k := range []string{"streams", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} {
|
|
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) != 1 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" {
|
|
t.Errorf("env = %v, want the broker file alone", p["env"])
|
|
}
|
|
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")
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if strings.Contains(string(src), f) {
|
|
t.Errorf("%s holds %s: the builder serves no request subject 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)
|
|
}
|
|
}
|