Files
mesh-controller/internal/link/handover_test.go
T
jochen 9006c82393
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
node hand-over: the terminal hands a directory used as found to the mesh, asked of the node's engine (hq issue 356)
A module's condition told the operator to run the node-engine's hand-over at the machine, as root (issue 339), and the mesh had no channel for it. Now node hand-over <node> <path> is the controller's terminal's — a node subcommand that is not a read, so every verb and mesh-cli outside the terminal refuse it — and asks that node's engine on mesh.node.<node>.ask.hand-over, a request only the controller may publish and only that node's engine may hear and answer (its grant gains the subject and the right to answer what it was asked). The line's node and path are judged before anything is asked; the engine's answer is printed, a refusal as a refusal. Every text addressed to the operator names the nox line (ADR 0272); the condition's words stay plain.
2026-10-09 18:38:09 +02:00

104 lines
3.9 KiB
Go

package link
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/testbus"
)
// The hand-over's ask and answer hold these field names; the engine's side holds the same list (mesh-host
// internal/link, TestTheHandOverAskAndAnswerKeepTheirFieldNames).
func TestTheHandOverAskAndAnswerKeepTheirFieldNames(t *testing.T) {
body, _ := json.Marshal(HandOverAsk{Path: "/srv/notes", By: "jo through mesh-cli on anchor"})
if got := keysIn(t, body); got != "by path" {
t.Fatalf("the ask's fields are %q", got)
}
body, _ = json.Marshal(HandOverAnswer{Said: "s", Refused: "r"})
if got := keysIn(t, body); got != "refused said" {
t.Fatalf("the answer's fields are %q", got)
}
if broker.AskHandOverSubject("laptop") != "mesh.node.laptop.ask.hand-over" {
t.Fatalf("the subject is %q", broker.AskHandOverSubject("laptop"))
}
}
// The ask reaches the machine's engine on its own subject and its answer comes back whole: what it recorded, or
// its refusal as the engine worded it. A machine with no engine listening is said as not answering, and an
// answer that is neither is refused rather than read as a record.
func TestAHandOverIsAskedOfTheMachineAndItsAnswerComesBack(t *testing.T) {
url := testbus.URL(t)
conn, err := nats.Connect(url)
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
var heard HandOverAsk
engine, err := conn.Subscribe(broker.AskHandOverSubject("laptop"), func(msg *nats.Msg) {
_ = json.Unmarshal(msg.Data, &heard)
switch heard.Path {
case "/srv/notes":
body, _ := json.Marshal(HandOverAnswer{Said: "/srv/notes (notes.data) is handed to the mesh by " + heard.By})
_ = msg.Respond(body)
case "/srv/empty":
_ = msg.Respond([]byte(`{}`))
default:
body, _ := json.Marshal(HandOverAnswer{Refused: heard.Path + " is not a directory this machine uses as found; nothing was handed over"})
_ = msg.Respond(body)
}
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = engine.Unsubscribe() })
ctx := context.Background()
a, err := AskHandOver(ctx, conn, "laptop", "/srv/notes", "jo through mesh-cli on anchor", 5*time.Second)
if err != nil || a.Refused != "" || !strings.Contains(a.Said, "handed to the mesh by jo through mesh-cli on anchor") {
t.Fatalf("answered %+v, %v", a, err)
}
if heard.Path != "/srv/notes" || heard.By != "jo through mesh-cli on anchor" {
t.Fatalf("the engine heard %+v", heard)
}
a, err = AskHandOver(ctx, conn, "laptop", "/srv/other", "jo", 5*time.Second)
if err != nil || a.Said != "" || !strings.Contains(a.Refused, "nothing was handed over") {
t.Fatalf("a refusal came back as %+v, %v", a, err)
}
if _, err := AskHandOver(ctx, conn, "laptop", "/srv/empty", "jo", 5*time.Second); err == nil ||
!strings.Contains(err.Error(), "neither") {
t.Fatalf("an empty answer was taken: %v", err)
}
if _, err := AskHandOver(ctx, conn, "anchor", "/srv/notes", "jo", 5*time.Second); err == nil ||
!strings.Contains(err.Error(), "node-engine") {
t.Fatalf("a machine with no engine listening: %v", err)
}
if _, err := AskHandOver(ctx, nil, "anchor", "/srv/notes", "jo", time.Second); err == nil {
t.Fatal("asked with no bus")
}
}
// A machine's grant hears its own hand-over ask and nobody else's, and may answer it: the one request a node is
// asked (novox/hq issue 356).
func TestAMachineHearsItsOwnHandOverAskAndMayAnswerIt(t *testing.T) {
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindNode, Node: "laptop", PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
var hears, other bool
for _, s := range perms.Subscribe {
hears = hears || s == "mesh.node.laptop.ask.hand-over"
other = other || s == "mesh.node.anchor.ask.hand-over"
}
if !hears || other || !perms.AllowResponses {
t.Fatalf("a machine's grant: hears its own %v, another's %v, answers %v (%v)", hears, other, perms.AllowResponses,
perms.Subscribe)
}
}