The group try of 2026-11-06 10:07 failed its composition on all four machines because mesh-compose, the node-engine's composer a check builds at the head, loaded no seat list once the compiled one was deleted (ADR 0635 §1): the uplink's holder no longer stepped back from the resolver file, and no holder of the registry seats was known. It now takes the request's seats over the foundation's bootstrapped ones, as the node-engine takes the bus's; without any it says each machine not judged (ADR 0636 §1, ADR 0593 §9), never a refusal or pass made without them. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
383 lines
16 KiB
Go
383 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"github.com/novox/mesh-host/internal/buckets/bucketstest"
|
|
"go/build"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
|
|
"github.com/novox/mesh-host/internal/beside"
|
|
"github.com/novox/mesh-host/internal/compose"
|
|
"github.com/novox/mesh-host/internal/composer"
|
|
)
|
|
|
|
// captured is every manifest of the catalogue captured in testdata/beside (internal/beside), raw, by module name.
|
|
func captured(t *testing.T) map[string]composer.Manifest {
|
|
t.Helper()
|
|
read, err := readCatalogue(filepath.Dir(beside.Catalogue(t)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(read) < 100 {
|
|
t.Fatalf("the captured catalogue holds %d manifests, fewer than the catalogue's", len(read))
|
|
}
|
|
return read
|
|
}
|
|
|
|
// meshOf is a mesh of three machines over the captured catalogue, each assigned real modules, the input the assigner
|
|
// composes with (nodes and their assignments, holdings from the claims): anchor runs the bus and the store, laptop a
|
|
// desktop's modules, ace a module whose requirement is provided elsewhere.
|
|
func meshOf() compose.Input {
|
|
return compose.Input{Nodes: []compose.Node{
|
|
{Name: "anchor", Assigned: []string{"nats", "postgres"}},
|
|
{Name: "laptop", Assigned: []string{"sudo", "zsh"}},
|
|
{Name: "ace", Assigned: []string{"mesh-reads", "nats"}},
|
|
}}
|
|
}
|
|
|
|
func request(t *testing.T, depth string) composer.Request {
|
|
t.Helper()
|
|
r := composer.Request{V: composer.Version, Depth: depth, HoldingsFromClaims: true,
|
|
Machines: []string{"anchor", "laptop", "ace"}, Seats: seats(t),
|
|
Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
|
|
return withInput(t, r, meshOf())
|
|
}
|
|
|
|
// seats is the seat list a request carries, as the checker hands the one its reader holds (ADR 0635 §1, ADR 0636 §4).
|
|
func seats(t *testing.T) []json.RawMessage {
|
|
t.Helper()
|
|
var out []json.RawMessage
|
|
for _, d := range seatlisttest.Definitions() {
|
|
raw, err := json.Marshal(d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out = append(out, raw)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// withInput is r with in as its input, as a caller puts it on the wire: the request's shape carries the input as JSON.
|
|
func withInput(t *testing.T, r composer.Request, in compose.Input) composer.Request {
|
|
t.Helper()
|
|
raw, err := json.Marshal(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(raw, &r.Input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func runJSON(t *testing.T, args []string, in []byte) (int, composer.Verdict, string) {
|
|
t.Helper()
|
|
var out, errOut bytes.Buffer
|
|
code := run(args, bytes.NewReader(in), &out, &errOut)
|
|
var v composer.Verdict
|
|
if code == 0 {
|
|
if err := json.Unmarshal(out.Bytes(), &v); err != nil {
|
|
t.Fatalf("the verdict is not JSON: %v\n%s", err, out.String())
|
|
}
|
|
}
|
|
return code, v, errOut.String()
|
|
}
|
|
|
|
func encode(t *testing.T, r composer.Request) []byte {
|
|
t.Helper()
|
|
raw, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// 1621/AC1: the command reads the plain input and the manifests on standard input, validates every manifest, composes
|
|
// the machines named and writes one verdict: per machine composes or the reason, per manifest accepted or the field
|
|
// refused.
|
|
func TestTheCommandWritesOneVerdictPerManifestAndMachine(t *testing.T) {
|
|
r := request(t, composer.DepthResolve)
|
|
r.Cases[0].Manifests["later"] = composer.Manifest{Raw: json.RawMessage(`{"module":"later","version":"1","shares":["x"]}`)}
|
|
r.Cases[0].Manifests["refused"] = composer.Manifest{Raw: json.RawMessage(`{"module":"refused","version":"1"}`)}
|
|
r.Cases[0].Verdicts = bucketstest.Raw(map[string]meshcatalog.VerdictsByDigestValue{"refused": {V: 1, Module: "refused", Judge: "src2:j", State: meshcatalogclient.VerdictRefused, Problems: []string{"it is wrong"}}})
|
|
code, v, stderr := runJSON(t, nil, encode(t, r))
|
|
if code != 0 {
|
|
t.Fatalf("exit %d: %s", code, stderr)
|
|
}
|
|
if len(v.Cases) != 1 || v.Cases[0].Name != "trunk" || v.SDK == "" {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
c := v.Cases[0]
|
|
// ADR 0578 §5, §6: the composer judges nothing; it sets an unknown field aside and names it, and holds a manifest
|
|
// its verdict refuses, named in the indexer's words.
|
|
if got := c.Manifests["later"]; !got.Accepted || !strings.Contains(got.SetAside, `"shares"`) {
|
|
t.Errorf("the manifest with an unknown field reads %+v, want accepted naming the field set aside", got)
|
|
}
|
|
if got := c.Manifests["refused"]; got.Accepted || !strings.Contains(got.Refused, "refused the manifest of refused: it is wrong") {
|
|
t.Errorf("the refused manifest reads %+v, want refused in the verdict's words", got)
|
|
}
|
|
if got := c.Manifests["nats"]; !got.Accepted {
|
|
t.Errorf("nats reads %+v, want accepted", got)
|
|
}
|
|
if len(c.Machines) != 3 {
|
|
t.Fatalf("machines = %v, want the three named", c.Machines)
|
|
}
|
|
for name, m := range c.Machines {
|
|
if !m.Composes && m.Reason == "" {
|
|
t.Errorf("%s does not compose and says no reason", name)
|
|
}
|
|
}
|
|
|
|
// Issue 1966, the group try of 2026-11-06 10:07: the composer judges against the seat list its request carries, the
|
|
// node-engine's from the bus (ADR 0635 §1). The uplink's holder steps back from the resolver file for the resolver's
|
|
// holder by the seat's definition; without the list nothing is judged (ADR 0636 §1), never "both declare the path".
|
|
resolverMachine := func(withSeats bool) composer.Machine {
|
|
r := composer.Request{V: composer.Version, Depth: composer.DepthCompose, HoldingsFromClaims: true,
|
|
Machines: []string{"laptop"}, Cases: []composer.Case{{Name: "trunk", Manifests: captured(t)}}}
|
|
if withSeats {
|
|
r.Seats = seats(t)
|
|
}
|
|
r = withInput(t, r, compose.Input{Nodes: []compose.Node{{Name: "laptop",
|
|
Assigned: []string{"networkmanager", "systemd-resolved", "pacman", "systemd", "dnsmasq"},
|
|
Capabilities: map[string]bool{"file-references": true, "privileged": true, "service-manager": true, "package-manager": true, "uplink-networkmanager": true}}}})
|
|
code, v, stderr := runJSON(t, nil, encode(t, r))
|
|
if code != 0 || len(v.Cases) != 1 {
|
|
t.Fatalf("exit %d: %s", code, stderr)
|
|
}
|
|
return v.Cases[0].Machines["laptop"]
|
|
}
|
|
if m := resolverMachine(true); strings.Contains(m.Reason+strings.Join(m.Problems, " "), "both declare") || m.NotJudged != "" {
|
|
t.Errorf("with the seat list, the uplink's and the resolver's holders on one machine: %+v, want the uplink's stepping back", m)
|
|
}
|
|
if m := resolverMachine(false); m.Composes || m.NotJudged != NoSeatList || m.Reason != "" {
|
|
t.Errorf("without a seat list: %+v, want not judged, saying why", m)
|
|
}
|
|
}
|
|
|
|
// 1621/AC3: over the captured catalogue and a mesh of three machines, the command's verdict equals, machine for
|
|
// machine and manifest for manifest, what the linked reader, resolution and composition answer, at both depths.
|
|
func TestTheCommandAnswersWhatTheLinkedCompositionAnswers(t *testing.T) {
|
|
for _, depth := range []string{composer.DepthResolve, composer.DepthCompose} {
|
|
r := request(t, depth)
|
|
code, v, stderr := runJSON(t, nil, encode(t, r))
|
|
if code != 0 {
|
|
t.Fatalf("%s: exit %d: %s", depth, code, stderr)
|
|
}
|
|
in := meshOf()
|
|
in.Catalogue = map[string]catalogue.Manifest{}
|
|
for name, m := range r.Cases[0].Manifests {
|
|
// A request without a list of predefined keys reads a fallback as the indexer does (Request.Predefined).
|
|
parsed, err := catalogue.Decode(m.Raw)
|
|
got := v.Cases[0].Manifests[name]
|
|
if (err == nil) != got.Accepted || (err != nil && err.Error() != got.Refused) {
|
|
t.Errorf("%s: the manifest of %s: linked %v, command %+v", depth, name, err, got)
|
|
}
|
|
if err == nil {
|
|
in.Catalogue[name] = parsed
|
|
}
|
|
}
|
|
in.Holdings = composer.HoldingsOf(in.Catalogue, in.Nodes)
|
|
composed := 0
|
|
for _, node := range r.Machines {
|
|
got := v.Cases[0].Machines[node]
|
|
var want composer.Machine
|
|
if depth == composer.DepthResolve {
|
|
if _, err := compose.Resolve(in, node); err != nil {
|
|
want = composer.Machine{Reason: err.Error()}
|
|
} else {
|
|
want = composer.Machine{Composes: true}
|
|
}
|
|
} else {
|
|
d, problems, err := compose.Compose(in, node)
|
|
switch {
|
|
case err != nil:
|
|
want = composer.Machine{Reason: err.Error()}
|
|
default:
|
|
want = composer.Machine{Composes: len(problems) == 0, Problems: problems, Refusals: d.Refusals()}
|
|
if len(problems) > 0 {
|
|
want.Reason = "the node-engine's validator refuses the declaration"
|
|
}
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("%s: %s: command %+v, linked %+v", depth, node, got, want)
|
|
}
|
|
if got.Composes {
|
|
composed++
|
|
} else {
|
|
t.Logf("%s: %s does not compose: %s %v", depth, node, got.Reason, got.Problems)
|
|
}
|
|
}
|
|
t.Logf("%s: %d of %d machines compose", depth, composed, len(r.Machines))
|
|
}
|
|
}
|
|
|
|
// 1621 note 6: a person composes a catalogue checkout (repository@ref checked out or archived: the command fetches
|
|
// nothing) for a node and reads its declaration: each path with its kind, owner and mode.
|
|
func TestACatalogueCheckoutIsComposedForANode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(dir, "modules", "demo"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifest := `{"module": "demo", "version": "1", "resources": [{"id": "state", "type": "directory", ` +
|
|
`"path": "/var/lib/demo", "mode": "0750", "owner": "root"}]}`
|
|
if err := os.WriteFile(filepath.Join(dir, "modules", "demo", "module.json"), []byte(manifest), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in := encode(t, withInput(t, composer.Request{V: composer.Version, Seats: seats(t)}, compose.Input{Nodes: []compose.Node{{Name: "laptop", Assigned: []string{"demo"}}}}))
|
|
code, v, stderr := runJSON(t, []string{"-catalogue", dir, "-node", "laptop", "-declarations"}, in)
|
|
if code != 0 {
|
|
t.Fatalf("exit %d: %s", code, stderr)
|
|
}
|
|
m := v.Cases[0].Machines["laptop"]
|
|
want := []composer.Path{{Path: "/var/lib/demo", Kind: "directory", Owner: "root", Mode: "0750", Module: ""}}
|
|
if !m.Composes || len(m.Declared) != 1 || m.Declared[0].Path != want[0].Path || m.Declared[0].Mode != "0750" ||
|
|
m.Declared[0].Owner != "root" || m.Declared[0].Kind != "directory" {
|
|
t.Fatalf("laptop = %+v, want it composed declaring %+v", m, want)
|
|
}
|
|
}
|
|
|
|
// A run step hands the command no standard input: the request is read from the file -request names, and a file that is
|
|
// not there is a request it cannot read.
|
|
func TestTheRequestIsReadFromAFile(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "request.json")
|
|
r := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "only", Manifests: map[string]composer.Manifest{
|
|
"demo": {Raw: json.RawMessage(`{"module":"demo","version":"1"}`)}}}}}
|
|
if err := os.WriteFile(file, encode(t, r), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, v, stderr := runJSON(t, []string{"-request", file}, nil)
|
|
if code != 0 || len(v.Cases) != 1 || !v.Cases[0].Manifests["demo"].Accepted {
|
|
t.Fatalf("exit %d (%s), verdict %+v", code, stderr, v)
|
|
}
|
|
if code, _, stderr := runJSON(t, []string{"-request", file + ".gone"}, nil); code != 1 || !strings.Contains(stderr, "cannot be read") {
|
|
t.Errorf("a request file that is not there: exit %d, %q", code, stderr)
|
|
}
|
|
}
|
|
|
|
// The exit codes: 1 for a request that cannot be read or run, 2 for wrong flags, each said on standard error.
|
|
func TestTheExitCodesSayWhatFailed(t *testing.T) {
|
|
unfilled := encode(t, composer.Request{V: composer.Version, Cases: []composer.Case{{Name: "c", Manifests: map[string]composer.Manifest{"x": {Digest: "abc"}}}}})
|
|
for _, c := range []struct {
|
|
name string
|
|
args []string
|
|
in string
|
|
code int
|
|
says string
|
|
}{
|
|
{"not JSON", nil, "{", 1, "not JSON"},
|
|
{"another version", nil, `{"v":2}`, 1, "version 2"},
|
|
{"a digest left unfilled", nil, string(unfilled), 1, "digest"},
|
|
{"an unknown depth", nil, `{"v":1,"depth":"deep"}`, 1, "depth"},
|
|
{"nothing to read", nil, "", 1, "no request"},
|
|
{"a wrong flag", []string{"-nope"}, "", 2, "nope"},
|
|
{"an argument", []string{"extra"}, "", 2, "unexpected"},
|
|
} {
|
|
var out, errOut bytes.Buffer
|
|
if got := run(c.args, strings.NewReader(c.in), &out, &errOut); got != c.code || !strings.Contains(errOut.String(), c.says) {
|
|
t.Errorf("%s: exit %d saying %q, want %d saying %q", c.name, got, errOut.String(), c.code, c.says)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ownPackages is every package of this repository the command imports, directly or not, with the packages each
|
|
// imports: what this repository adds to the composition; mesh-sdk's packages are judged in mesh-sdk.
|
|
func ownPackages(t *testing.T) map[string][]string {
|
|
t.Helper()
|
|
_, file, _, _ := runtime.Caller(0)
|
|
root := filepath.Join(filepath.Dir(file), "..", "..")
|
|
const module = "github.com/novox/mesh-host"
|
|
out := map[string][]string{}
|
|
var walk func(path string)
|
|
walk = func(path string) {
|
|
if _, done := out[path]; done {
|
|
return
|
|
}
|
|
rel := strings.TrimPrefix(strings.TrimPrefix(path, module), "/")
|
|
pkg, err := build.Default.ImportDir(filepath.Join(root, rel), 0)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", path, err)
|
|
}
|
|
out[path] = pkg.Imports
|
|
for _, imp := range pkg.Imports {
|
|
if strings.HasPrefix(imp, module+"/") {
|
|
walk(imp)
|
|
}
|
|
}
|
|
}
|
|
walk(composer.Package)
|
|
return out
|
|
}
|
|
|
|
// 1621/AC2 and AC4: the command imports, through every package of this repository it reaches, only the standard
|
|
// library, mesh-sdk, the contracts modules' pure packages (seats' types, clients' codecs), the delivery SDK's composer (the command's own request and verdict types), the builder's builds package (a build's value and its keys), the private network's overlay package (mesh-wireguard's own pure package: names, graph and
|
|
// generator, no bus) and this repository's own packages, and none that reaches a bus, a store, a network service or
|
|
// another process.
|
|
func TestTheCommandImportsNoBusStoreNetworkOrProcess(t *testing.T) {
|
|
forbidden := []string{"net/http", "net/rpc", "net/smtp", "os/exec", "database/sql", "plugin", "syscall"}
|
|
own := ownPackages(t)
|
|
if len(own) < 4 {
|
|
t.Fatalf("the command reaches %d packages of this repository; the walk found too few to judge", len(own))
|
|
}
|
|
for path, imports := range own {
|
|
for _, imp := range imports {
|
|
std := !strings.Contains(strings.SplitN(imp, "/", 2)[0], ".")
|
|
if !std && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-sdk/go") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/contracts/") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/sdk/composer") && !strings.HasPrefix(imp, "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds") && !strings.HasPrefix(imp, "github.com/novox/mesh-host/") {
|
|
t.Errorf("%s imports %s, which is neither the standard library, mesh-sdk nor this repository", path, imp)
|
|
}
|
|
for _, f := range forbidden {
|
|
if imp == f {
|
|
t.Errorf("%s imports %s", path, imp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1621/AC2: run with an empty network namespace, the command still writes its verdict: it needs no network. The test
|
|
// runs its own binary again under `unshare --net` as an unprivileged user's namespace; where the kernel or the sandbox
|
|
// allows no new namespace (a build module's run step is already one with no network), it says so and skips.
|
|
func TestTheCommandRunsWithNoNetwork(t *testing.T) {
|
|
if os.Getenv("MESH_COMPOSE_HELPER") == "1" {
|
|
os.Exit(run(nil, os.Stdin, os.Stdout, os.Stderr))
|
|
}
|
|
unshare, err := exec.LookPath("unshare")
|
|
if err != nil {
|
|
t.Skip("no unshare on this machine, so no empty network namespace can be made here")
|
|
}
|
|
if out, err := exec.Command(unshare, "--net", "--map-root-user", "true").CombinedOutput(); err != nil {
|
|
t.Skipf("this machine makes no unprivileged network namespace (%v: %s); a run step's own has no network", err, out)
|
|
}
|
|
r := request(t, composer.DepthResolve)
|
|
cmd := exec.Command(unshare, "--net", "--map-root-user", os.Args[0], "-test.run=^TestTheCommandRunsWithNoNetwork$")
|
|
cmd.Env = []string{"MESH_COMPOSE_HELPER=1"}
|
|
cmd.Stdin = bytes.NewReader(encode(t, r))
|
|
var out, errOut bytes.Buffer
|
|
cmd.Stdout, cmd.Stderr = &out, &errOut
|
|
if err := cmd.Run(); err != nil {
|
|
t.Fatalf("the command in an empty network namespace: %v: %s", err, errOut.String())
|
|
}
|
|
var v composer.Verdict
|
|
if err := json.Unmarshal(out.Bytes(), &v); err != nil || len(v.Cases) != 1 || len(v.Cases[0].Machines) != 3 {
|
|
t.Fatalf("no verdict from the command with no network: %v\n%s", err, out.String())
|
|
}
|
|
names := make([]string, 0, len(v.Cases[0].Machines))
|
|
for n := range v.Cases[0].Machines {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
t.Logf("composed with no network: %v", names)
|
|
}
|