A manifest names its toolchain by language, not in build.on, so the planner did not know a bundle depends on the module that publishes its toolchain and built the two in one tier: the bundle against the old toolchain, recorded as built from the new commit. The edge is read from the manifest, so it holds before any build recorded it, and a toolchain that moves rebuilds every bundle compiled in it.
299 lines
12 KiB
Go
299 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// Every verb the mesh-controller seat declares is one this binary can run, with the arguments the
|
|
// schema names and no other (novox/hq ADR 0154, ADR 0035).
|
|
func TestEveryDeclaredVerbHasACommandLine(t *testing.T) {
|
|
for _, v := range catalogue.ControllerVerbs {
|
|
if v.Name == "tools" {
|
|
continue
|
|
}
|
|
args := map[string]any{}
|
|
props, _ := v.Input["properties"].(map[string]any)
|
|
for name := range props {
|
|
args[name] = "x"
|
|
}
|
|
argv, err := argvFor(v.Name, args)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", v.Name, err)
|
|
continue
|
|
}
|
|
if argv[0] == "" {
|
|
t.Errorf("%s: empty command", v.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// `builds` given a build's id reads that build's log from the bus rather than listing builds
|
|
// (novox/hq ADR 0157).
|
|
func TestBuildsWithAnIdReadsThatBuildsLog(t *testing.T) {
|
|
argv, err := argvFor("builds", map[string]any{"log": "build-17"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(argv, " ") != "builds --log build-17" {
|
|
t.Fatalf("builds with a log id became %q", strings.Join(argv, " "))
|
|
}
|
|
}
|
|
|
|
// The build tool takes a repository as a URL or as its path on the forge holding the git seat, and
|
|
// says which it was given, so the command reads the path as a seat source rather than handing it to
|
|
// git as written (novox/hq issue 176). And it never waits: the id follows the build.
|
|
func TestTheBuildToolTellsAForgePathFromAURL(t *testing.T) {
|
|
argv, _ := argvFor("build", map[string]any{"repository": "novox/mesh-catalog", "path": "modules/x"})
|
|
if line := strings.Join(argv, " "); !strings.Contains(line, "--self") || !strings.Contains(line, "--wait 0") {
|
|
t.Fatalf("a forge path is a seat source, not waited for; got %q", line)
|
|
}
|
|
argv, _ = argvFor("build", map[string]any{"repository": "https://example.tld/o/r.git"})
|
|
if line := strings.Join(argv, " "); strings.Contains(line, "--self") {
|
|
t.Fatalf("a URL is cloned as given; got %q", line)
|
|
}
|
|
}
|
|
|
|
// `rotate` is one verb with two shapes (ADR 0114, issue 180): a pair credential by provision, or a
|
|
// module's own secret by machine, module and name.
|
|
func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) {
|
|
argv, _ := argvFor("rotate", map[string]any{"provision": "postgres-database", "consumer": "ace"})
|
|
if strings.Join(argv, " ") != "rotate postgres-database --consumer ace" {
|
|
t.Fatalf("a pair credential: %v", argv)
|
|
}
|
|
argv, _ = argvFor("rotate", map[string]any{"node": "ace", "module": "nodered", "secret": "api-token"})
|
|
if strings.Join(argv, " ") != "secret rotate ace nodered api-token" {
|
|
t.Fatalf("an own secret: %v", argv)
|
|
}
|
|
argv, _ = argvFor("rotate", map[string]any{"node": "ace"})
|
|
if strings.Join(argv, " ") != "rotate" {
|
|
t.Fatalf("half an own secret falls to the command's usage: %v", argv)
|
|
}
|
|
}
|
|
|
|
// `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198).
|
|
func TestSettingsSetsOrClearsALayer(t *testing.T) {
|
|
argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"})
|
|
if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --node ace` {
|
|
t.Fatalf("set on a machine: %v %v", argv, err)
|
|
}
|
|
argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq", "clear": "true"})
|
|
if strings.Join(argv, " ") != "settings clear dnsmasq" {
|
|
t.Fatalf("clear for the mesh: %v", argv)
|
|
}
|
|
argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq"})
|
|
if strings.Join(argv, " ") != "settings set dnsmasq" {
|
|
t.Fatalf("a set with no values falls to the command's usage: %v", argv)
|
|
}
|
|
}
|
|
|
|
// `issue` is `module issue` at a shell: the module and the machine, and nothing that would push. A
|
|
// module's bus account was mintable only from the controller's command line, so an agent working
|
|
// through the tools could not finish a rollout that gave a module one (novox/hq issue 191).
|
|
func TestIssueGivesAModuleItsAccountOnAMachine(t *testing.T) {
|
|
argv, err := argvFor("issue", map[string]any{"node": "ace", "module": "route-proxy"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Join(argv, " ") != "module issue route-proxy --node ace" {
|
|
t.Fatalf("issue runs %v", argv)
|
|
}
|
|
if _, err := argvFor("issue", map[string]any{"module": "route-proxy"}); err == nil {
|
|
t.Error("an account was issued without saying which machine reads it")
|
|
}
|
|
}
|
|
|
|
// A required argument missing is refused in the verb's own words, before anything runs.
|
|
func TestAVerbMissingWhatItNeedsIsRefused(t *testing.T) {
|
|
if _, err := argvFor("node", map[string]any{}); err == nil || !strings.Contains(err.Error(), `node needs "node"`) {
|
|
t.Fatalf("node without a machine was accepted: %v", err)
|
|
}
|
|
if _, err := argvFor("upgrade", map[string]any{}); err == nil {
|
|
t.Fatal("a verb the seat does not serve was accepted")
|
|
}
|
|
}
|
|
|
|
// A push and a build are sent, not waited for: the asker reads status, or the build's log by its
|
|
// id, for what happened. A repository given as a forge path is said to be one (issue 176).
|
|
func TestActsDoNotBlockTheCall(t *testing.T) {
|
|
argv, _ := argvFor("push", map[string]any{"node": "one"})
|
|
if strings.Join(argv, " ") != "push one --wait 0" {
|
|
t.Fatalf("push waits: %v", argv)
|
|
}
|
|
argv, _ = argvFor("build", map[string]any{"repository": "novox/x", "path": "modules/x"})
|
|
if strings.Join(argv, " ") != "build novox/x --wait 0 --self --path modules/x" {
|
|
t.Fatalf("build: %v", argv)
|
|
}
|
|
}
|
|
|
|
// What `tools` answers is the seats' records, with each verb's schema.
|
|
func TestToolsAnswersTheSeatsRecords(t *testing.T) {
|
|
handlers, behind, err := seatToolHandlers()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(behind) != 0 {
|
|
t.Fatalf("this build cannot run %v of its own seat's verbs", behind)
|
|
}
|
|
if len(handlers) != len(catalogue.ControllerVerbs) {
|
|
t.Fatalf("%d handlers for %d verbs", len(handlers), len(catalogue.ControllerVerbs))
|
|
}
|
|
answer := seatTools()
|
|
seats, _ := answer["seats"].([]map[string]any)
|
|
var found bool
|
|
for _, s := range seats {
|
|
if s["seat"] == catalogue.ControllerSeatName {
|
|
found = true
|
|
tools, _ := s["tools"].([]map[string]any)
|
|
if len(tools) != len(catalogue.ControllerVerbs) || tools[0]["input"] == nil {
|
|
t.Fatalf("the controller seat's tools are not listed in full: %v", tools)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("the mesh-controller seat is not in the listing")
|
|
}
|
|
}
|
|
|
|
// A JSON verb's answer is parsed from what the command wrote to standard output alone; a warning it
|
|
// printed beside the document does not take the document away. The test binary stands in for the
|
|
// controller: `-test.run` with a name that matches nothing prints `ok` and a warning about no tests.
|
|
func TestAJSONVerbsAnswerIsItsStandardOutput(t *testing.T) {
|
|
jsonVerbs["-test.run"] = true
|
|
t.Cleanup(func() { delete(jsonVerbs, "-test.run") })
|
|
answer, err := runVerb(t.Context(), []string{"-test.run", "TestAnswerEcho", "-test.v"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !answer.OK {
|
|
t.Fatalf("the command failed: %s", answer.Output)
|
|
}
|
|
if !strings.Contains(answer.Output, "PASS") {
|
|
t.Fatalf("stderr and stdout are both what the command said: %s", answer.Output)
|
|
}
|
|
}
|
|
|
|
// `command` is the generic verb: the command line as given, split as a shell would, nothing added —
|
|
// so an operator's `node account g14 jochen` is one call through the console rather than a shell on
|
|
// the control node (novox/hq ADR 0154, ADR 0175).
|
|
func TestCommandRunsTheLineAsGiven(t *testing.T) {
|
|
argv, err := argvFor("command", map[string]any{"command": "node account g14 jochen"})
|
|
if err != nil || strings.Join(argv, " ") != "node account g14 jochen" {
|
|
t.Fatalf("a plain line: %v %v", argv, err)
|
|
}
|
|
argv, err = argvFor("command", map[string]any{"command": `settings set dnsmasq '{"a": "b c"}' --node ace`})
|
|
if err != nil || len(argv) != 6 || argv[3] != `{"a": "b c"}` {
|
|
t.Fatalf("a quoted word stays one word: %q %v", argv, err)
|
|
}
|
|
argv, err = argvFor("command", map[string]any{"command": `node add "the box" --adopted`})
|
|
if err != nil || len(argv) != 4 || argv[2] != "the box" {
|
|
t.Fatalf("double quotes group: %q %v", argv, err)
|
|
}
|
|
if _, err := argvFor("command", map[string]any{"command": " "}); err == nil {
|
|
t.Fatal("an empty line was accepted")
|
|
}
|
|
if _, err := argvFor("command", map[string]any{"command": `node "unclosed`}); err == nil {
|
|
t.Fatal("an unclosed quote was accepted")
|
|
}
|
|
}
|
|
|
|
// A verb in the row that this binary cannot run does not take the control plane off the bus: the
|
|
// rest are served, the unknown one answers the reason, and the start-up names it (novox/hq ADR
|
|
// 0185). One unknown word cost the mesh ten minutes of silence on 2026-10-02, recoverable only by
|
|
// a person running the binary by hand — the push that would have repaired it needs the control
|
|
// plane that was down.
|
|
func TestARowAheadOfThisBuildIsServedAnyway(t *testing.T) {
|
|
seat, known := catalogue.SeatNamed(catalogue.ControllerSeatName)
|
|
if !known {
|
|
t.Fatal("no controller seat")
|
|
}
|
|
// The row as a newer control plane would have written it: every verb this build knows, and one
|
|
// it does not.
|
|
widened := seat
|
|
widened.Serves = append(append([]catalogue.Verb{}, seat.Serves...),
|
|
catalogue.Verb{Name: "teleport", Description: "a verb from a build that does not exist yet"})
|
|
rows := catalogue.DefaultSeats()
|
|
for i := range rows {
|
|
if rows[i].Name == catalogue.ControllerSeatName {
|
|
rows[i] = widened
|
|
}
|
|
}
|
|
catalogue.UseSeats(rows)
|
|
t.Cleanup(func() { catalogue.UseSeats(catalogue.DefaultSeats()) })
|
|
|
|
handlers, behind, err := seatToolHandlers()
|
|
if err != nil {
|
|
t.Fatalf("a row with one unknown verb refused to serve at all: %v", err)
|
|
}
|
|
if len(behind) != 1 || behind[0] != "teleport" {
|
|
t.Fatalf("the verbs this build cannot run were reported as %v", behind)
|
|
}
|
|
if len(handlers) != len(widened.Serves) {
|
|
t.Fatalf("%d handlers for %d verbs in the row", len(handlers), len(widened.Serves))
|
|
}
|
|
for _, known := range []string{"status", "nodes", "push"} {
|
|
if handlers[known] == nil {
|
|
t.Errorf("%s is not served although this build knows it", known)
|
|
}
|
|
}
|
|
_, err = handlers["teleport"](context.Background(), nil)
|
|
if err == nil {
|
|
t.Fatal("the unknown verb answered as though it had run")
|
|
}
|
|
for _, want := range []string{"teleport", "cannot run it", "behind"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the answer does not say %q: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0195: the console's discovery reads the machines and the modules; they answer as JSON,
|
|
// as status and seats do, so nothing parses a printed column.
|
|
func TestTheNodesAndModulesVerbsAnswerAsJSON(t *testing.T) {
|
|
for verb, want := range map[string]string{"nodes": "[node list --json]", "modules": "[module list --json]"} {
|
|
argv, err := argvFor(verb, map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprint(argv) != want {
|
|
t.Errorf("%s runs %v, want %s", verb, argv, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq ADR 0197: the controller announces exactly the verbs it serves, each on the subject and
|
|
// queue it serves it on, with the seat's own description and schema, in NATS's services format.
|
|
func TestTheControllerAnnouncesTheVerbsItServes(t *testing.T) {
|
|
handlers, _, err := seatToolHandlers()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info := seatAnnouncement(handlers)
|
|
if info.Name != catalogue.ControllerSeatName || info.ID == "" || info.Version == "" {
|
|
t.Fatalf("the service is not named for the seat: %+v", info.ServiceIdentity)
|
|
}
|
|
if len(info.Endpoints) != len(handlers) {
|
|
t.Fatalf("%d endpoints announced for %d verbs served", len(info.Endpoints), len(handlers))
|
|
}
|
|
for _, e := range info.Endpoints {
|
|
verb := e.Metadata["tool"]
|
|
if _, served := handlers[verb]; !served || e.Name != catalogue.ControllerSeatName+"__"+verb {
|
|
t.Errorf("%s (%s) is announced and not served under that name", e.Name, verb)
|
|
}
|
|
if e.Metadata["kind"] != "seat" || e.Metadata["seat"] != catalogue.ControllerSeatName {
|
|
t.Errorf("%s is not announced as the seat's verb: %v", e.Name, e.Metadata)
|
|
}
|
|
if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, verb) || e.QueueGroup != "seat."+catalogue.ControllerSeatName {
|
|
t.Errorf("%s is announced on %s/%s, not where it is served", e.Name, e.Subject, e.QueueGroup)
|
|
}
|
|
if e.Metadata["description"] == "" || e.Metadata["schema"] == "" || e.Metadata["scope"] != "mesh" {
|
|
t.Errorf("%s is announced without its description, schema or scope: %v", e.Name, e.Metadata)
|
|
}
|
|
}
|
|
}
|