build_node_fetch takes buildstep.FetchRequest (dir and paths narrow the workspace) and an
optional package {repository, commit, ref}: the group's package member, built and packed
shut off and published nowhere. Each member's lock decides its SDK: the group's packed
file when the version and integrity are the lock's, else the registry's (integrity
checked); a version the registry lacks fails waits for: @novox/mesh-sdk@<version>. A
package artifact is built by its own script and packed with npm pack, shut off; only a
trunk build publishes it, from this process with the write credential (never in a
workspace): absent is published, the same file is done, another is version taken:. The
registry configuration file now lies outside every workspace.
241 lines
9.5 KiB
Go
241 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// The catalogue's root and this module's directory.
|
|
const (
|
|
catalogueRoot = "../../../.."
|
|
moduleDir = "../.."
|
|
)
|
|
|
|
func readManifest(t *testing.T) catalogue.Manifest {
|
|
t.Helper()
|
|
m, err := catalogue.ReadTreeManifest(catalogueRoot, moduleDir)
|
|
if err != nil {
|
|
t.Fatalf("mesh-sdk's validator refuses module.json: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func resource(t *testing.T, m catalogue.Manifest, id string) map[string]any {
|
|
t.Helper()
|
|
for _, r := range m.Resources {
|
|
if r["id"] == id {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("module.json declares no resource %q", id)
|
|
return nil
|
|
}
|
|
|
|
// 1488/AC1: mesh-sdk's validator accepts the module (alone on a shelf too), and it runs as mesh-build-node with its
|
|
// home at mode 0700, requires user-namespaces and declares the four packages and carries toolchain/package-lock.json.
|
|
func TestManifestIsValidAndDeclaresTheAccountTheCapabilityAndThePackages(t *testing.T) {
|
|
m := readManifest(t)
|
|
if problems := catalogue.CatalogueProblems(catalogue.Shelf{m.Module: m}); len(problems) > 0 {
|
|
t.Errorf("the catalogue's rules refuse it: %v", problems)
|
|
}
|
|
if m.Module != "mesh-build-node" || m.RunsAs != "mesh-build-node" {
|
|
t.Errorf("module %q runs as %q, want mesh-build-node as itself", m.Module, m.RunsAs)
|
|
}
|
|
account := resource(t, m, "account")
|
|
if account["type"] != "user" || account["name"] != "mesh-build-node" || account["shell"] != "/usr/bin/nologin" || account["home"] != "/var/lib/mesh-build-node" {
|
|
t.Errorf("account = %v", account)
|
|
}
|
|
home := resource(t, m, "state")
|
|
if home["type"] != "directory" || home["mode"] != "0700" || home["owner"] != "mesh-build-node" || home["place"] != "." {
|
|
t.Errorf("home = %v, want its directory at mode 0700 owned by the account", home)
|
|
}
|
|
if !slices.Contains(m.Capabilities, "user-namespaces") {
|
|
t.Errorf("capabilities = %v, want user-namespaces", m.Capabilities)
|
|
}
|
|
packages := []string{}
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "package" {
|
|
packages = append(packages, r["package"].(string))
|
|
}
|
|
}
|
|
slices.Sort(packages)
|
|
if want := []string{"bubblewrap", "git", "openssh"}; !slices.Equal(packages, want) {
|
|
t.Errorf("packages = %v, want %v", packages, want)
|
|
}
|
|
}
|
|
|
|
// The work and cache directories are outside the home (ADR 0466 §1), owned by the account.
|
|
func TestWorkAndCacheDirectoriesLieOutsideTheHome(t *testing.T) {
|
|
m := readManifest(t)
|
|
for _, id := range []string{"work", "cache"} {
|
|
r := resource(t, m, id)
|
|
path, _ := r["path"].(string)
|
|
if path == "" || path == "/var/lib/mesh-build-node" || len(path) > len("/var/lib/mesh-build-node") && path[:len("/var/lib/mesh-build-node/")] == "/var/lib/mesh-build-node/" {
|
|
t.Errorf("%s is at %q, inside or at the home", id, path)
|
|
}
|
|
if r["owner"] != "mesh-build-node" {
|
|
t.Errorf("%s is owned by %v", id, r["owner"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// The states are the three the module keeps, no seat is claimed (ADR 0469 §6), and the health is the module's own
|
|
// tool, which the manifest also lists among its tools.
|
|
func TestManifestKeepsItsStatesClaimsNoSeatAndDeclaresItsHealthTool(t *testing.T) {
|
|
m := readManifest(t)
|
|
states := []string{}
|
|
for _, s := range m.State {
|
|
states = append(states, s.Name)
|
|
}
|
|
slices.Sort(states)
|
|
if want := []string{"keys", "steps", "toolchains"}; !slices.Equal(states, want) {
|
|
t.Errorf("state = %v, want %v", states, want)
|
|
}
|
|
if len(m.Claims) != 0 {
|
|
t.Errorf("claims = %v, want no seat", m.Claims)
|
|
}
|
|
if !slices.Contains(m.Tools, HealthTool) {
|
|
t.Errorf("tools = %v lack %s", m.Tools, HealthTool)
|
|
}
|
|
raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
|
if !strings.Contains(string(raw), `"tool": "`+HealthTool+`"`) {
|
|
t.Errorf("runtime-health does not name %s", HealthTool)
|
|
}
|
|
}
|
|
|
|
// Every tool the manifest names is served, and every tool served is named.
|
|
func TestTheToolsServedAreTheToolsTheManifestNames(t *testing.T) {
|
|
m := readManifest(t)
|
|
served := []string{}
|
|
for _, tool := range (&Module{}).Tools((&Module{}).StepTools()) {
|
|
served = append(served, tool.Name)
|
|
}
|
|
slices.Sort(served)
|
|
named := slices.Clone(m.Tools)
|
|
slices.Sort(named)
|
|
if !slices.Equal(served, named) {
|
|
t.Errorf("served %v, manifest names %v", served, named)
|
|
}
|
|
}
|
|
|
|
// 1488/AC1: the module carries the committed lock beside its package.json, and the manifest unpacks them into the
|
|
// home's toolchain directory, where the program reads them.
|
|
func TestTheModuleCarriesItsToolchainLockAndTheManifestPlacesIt(t *testing.T) {
|
|
m := readManifest(t)
|
|
for _, name := range []string{"package.json", "package-lock.json"} {
|
|
if _, err := os.Stat(filepath.Join(moduleDir, "toolchain", name)); err != nil {
|
|
t.Errorf("the module carries no toolchain/%s: %v", name, err)
|
|
}
|
|
}
|
|
if !slices.Contains(m.Requires, "node-toolchain") {
|
|
t.Errorf("requires = %v lack node-toolchain, which provides the nodejs and npm packages (issue 1573)", m.Requires)
|
|
}
|
|
if !slices.Contains(m.Requires, "npm-package-registry") {
|
|
t.Errorf("requires = %v lack npm-package-registry, which the install resolves @novox from", m.Requires)
|
|
}
|
|
}
|
|
|
|
// 1488/AC1: the lock names the versions of the recipe: typescript 5.9.3, esbuild 0.25.12, @novox/mesh-sdk 0.1.14
|
|
// and nats, all by the lock's own hash (read from the file the module carries).
|
|
func TestTheLockNamesTheToolchainVersions(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "toolchain", "package-lock.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var lock struct {
|
|
Packages map[string]struct {
|
|
Version string `json:"version"`
|
|
} `json:"packages"`
|
|
}
|
|
if err := json.Unmarshal(raw, &lock); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for pkg, want := range map[string]string{"typescript": "5.9.3", "esbuild": "0.25.12", "@novox/mesh-sdk": "0.1.14", "nats": "2.29.3", "@types/node": "22.20.1"} {
|
|
if got := lock.Packages["node_modules/"+pkg].Version; got != want {
|
|
t.Errorf("%s = %q in the lock, want %s", pkg, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1488 (the catalogue's manifest check): a manifest names no host, the mesh being name-agnostic. The tree-read
|
|
// manifest carries each template's text as the resource's content, and the toolchain's lock names registries, so it is
|
|
// shipped as an archive artifact instead, never as a template. No resource of the module, read as the mesh reads it,
|
|
// carries content naming a host, and the raw module.json names none either.
|
|
func TestNoResourceCarriesContentNamingAHost(t *testing.T) {
|
|
host := regexp.MustCompile(`(?i)://[a-z0-9]|\b[a-z0-9-]+(\.[a-z0-9-]+)*\.(com|org|net|be|io|dev)\b`)
|
|
m := readManifest(t)
|
|
for i, r := range m.Resources {
|
|
if c, _ := r["content"].(string); host.MatchString(c) {
|
|
t.Errorf("resources[%d] (%v) carries content naming %q", i, r["id"], host.FindString(c))
|
|
}
|
|
}
|
|
raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
|
if h := host.FindString(string(raw)); h != "" {
|
|
t.Errorf("module.json names %q", h)
|
|
}
|
|
}
|
|
|
|
// 1488: the toolchain's package.json and lock are shipped in the archive artifact `toolchain` and unpacked into the
|
|
// home's toolchain directory, where the program reads them.
|
|
func TestTheToolchainFilesAreAnArchiveUnpackedIntoTheHome(t *testing.T) {
|
|
m := readManifest(t)
|
|
r := resource(t, m, "toolchain-dir")
|
|
if r["type"] != "archive" || r["path"] != "${dir:state}/toolchain" || r["owner"] != "mesh-build-node" {
|
|
t.Errorf("toolchain-dir = %v, want an archive unpacked at ${dir:state}/toolchain", r)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(moduleDir, "toolchain", "package-lock.json")); err != nil {
|
|
t.Errorf("the module carries no toolchain/package-lock.json: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1309-S1: every run step runs in bubblewrap, so the module declares its package (the lead's decision: each build module
|
|
// that runs a step through it declares it), beside the user-namespaces capability bubblewrap needs.
|
|
func TestTheModuleDeclaresBubblewrapAndTheCapabilityItNeeds(t *testing.T) {
|
|
m := readManifest(t)
|
|
if r := resource(t, m, "bubblewrap"); r["type"] != "package" || r["package"] != "bubblewrap" {
|
|
t.Errorf("the bubblewrap resource is %v, want the package bubblewrap", r)
|
|
}
|
|
if !slices.Contains(m.Capabilities, "user-namespaces") {
|
|
t.Errorf("capabilities %v lack user-namespaces", m.Capabilities)
|
|
}
|
|
}
|
|
|
|
// 1587-S5 (ADR 0498 §2): the module requires the artifact store and is told its address in a file of its home, which no
|
|
// run step is given (a run step has no network either, ADR 0448).
|
|
func TestTheModuleRequiresTheArtifactStoreAndIsToldItsAddress(t *testing.T) {
|
|
m := readManifest(t)
|
|
if !slices.Contains(m.Requires, "artifact-store") {
|
|
t.Errorf("requires %v lack artifact-store", m.Requires)
|
|
}
|
|
// The validator reads the template (files/registry) into the resource's content.
|
|
r := resource(t, m, "registry")
|
|
if content, _ := r["content"].(string); r["type"] != "file" || r["path"] != "${dir:state}/registry" || !strings.Contains(content, "${seat:mesh-artifact-store:reach}") {
|
|
t.Errorf("the registry resource is %v", r)
|
|
}
|
|
}
|
|
|
|
// 1587-S17 (ADR 0498 §5): the module publishes a trunk package itself, so its contribution to the package registry asks
|
|
// for write access; the credential stays with the module's own process (a run step has neither the home nor a network).
|
|
func TestTheModuleContributesWriteAccessToThePackageRegistry(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var mj struct {
|
|
Contributes map[string]map[string]any `json:"contributes"`
|
|
}
|
|
if err := json.Unmarshal(raw, &mj); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := mj.Contributes["npm-package-registry"]["access"]; got != "write" {
|
|
t.Errorf("npm-package-registry access = %v, want write", got)
|
|
}
|
|
}
|