Files
mesh-tools/node-tools/internal/provide/provide_test.go
T

197 lines
6.9 KiB
Go

package provide
import (
"encoding/json"
"os"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provisions"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// claude-code's facts.json template, as the catalogue keeps it (modules/claude-code/files/facts.json).
const factsTemplate = `{
"node": "${machine:name}",
"console": "http://127.0.0.1:${bound:mcp-endpoint:port}/mcp"
}
`
// factsToday is that file as the controller composes it today, from the port node-tools serves.
const factsToday = `{
"node": "anchor",
"console": "http://127.0.0.1:4270/mcp"
}
`
// fill is what the consumer's client does with the template: the machine's name, and each ${bound:<provision>:<key>}
// from the provider's entry.
func fill(t *testing.T, template string, entry map[string]any) string {
t.Helper()
values, _ := entry["values"].(map[string]any)
out := strings.ReplaceAll(template, "${machine:name}", "anchor")
for key, v := range values {
raw, _ := json.Marshal(v)
out = strings.ReplaceAll(out, "${bound:mcp-endpoint:"+key+"}", string(raw))
}
return out
}
func TestTheRuntimeAnswersAHandedConsumersMcpEndpointThroughTheHelper(t *testing.T) {
mesh := mt.New(t)
bound := mesh.Bucket(t, "node-tools_bound")
plans := mesh.Bucket(t, provisions.PlansBucket)
provs := mesh.Bucket(t, provisions.ProvisionsBucket)
mem := mt.MembershipOf("node-tools", "anchor", false, nil)
mem.State = []bus.StateIssued{
{Name: "bound", Bucket: "node-tools_bound", Writes: true},
{Name: provisions.PlansRead, Bucket: provisions.PlansBucket},
{Name: provisions.ProvisionsRead, Bucket: provisions.ProvisionsBucket},
}
mesh.Issue(t, mem)
// claude-code, handed on this machine, is bound to node-tools here for mcp-endpoint: its plan and its provision
// key, written as mesh-provisioner writes them, with mesh-sdk's encoders (ADR 0570 clauses 1-3, issue 1954).
plan, err := provisions.EncodePlan(provisions.Plan{Node: "anchor", Module: "claude-code", Plan: catalogue.Plan{
Provisions: []catalogue.PlanProvision{{Provision: "mcp-endpoint", Provider: "anchor", ProviderModule: "node-tools",
At: "127.0.0.1", From: "anchor", As: "mesh_anchor_claude_code"}}}})
if err != nil {
t.Fatal(err)
}
if _, err := plans.Put("anchor.claude-code", plan); err != nil {
t.Fatal(err)
}
key, err := provisions.Key("anchor", "claude-code", "mcp-endpoint", "")
if err != nil {
t.Fatal(err)
}
asked, err := provisions.Encode(provisions.Provision{Provision: "mcp-endpoint", State: provisions.Asked,
Consumer: provisions.End{Node: "anchor", Module: "claude-code"}, Provider: provisions.End{Node: "anchor", Module: "node-tools"},
At: time.Now()})
if err != nil {
t.Fatal(err)
}
if _, err := provs.Put(key, asked); err != nil {
t.Fatal(err)
}
conn, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: "node-tools", Node: "anchor"})
if err != nil {
t.Fatal(err)
}
conn.Logf = func(string, ...any) {}
t.Cleanup(conn.Close)
conn.Follow("node-tools")
said := &mt.Logs{}
t.Cleanup(Start(conn, "node-tools", "anchor", 20*time.Millisecond, said.Logf))
var entry map[string]any
mt.Until(t, func() error {
kept, err := bound.Get("anchor.claude-code.mcp-endpoint")
if err != nil {
return err
}
return json.Unmarshal(kept.Value(), &entry)
})
values, _ := entry["values"].(map[string]any)
for key, want := range map[string]any{"port": float64(4270), "at": "127.0.0.1", "from": "anchor"} {
if values[key] != want {
t.Errorf("the entry's %s is %v, want %v (entry %v)", key, values[key], want, entry)
}
}
if entry["as"] != "mesh_anchor_claude_code" {
t.Errorf("the entry's as is %v", entry["as"])
}
if c, has := entry["credential"]; has {
t.Errorf("an offer with no credential names one: %v", c)
}
if got := fill(t, factsTemplate, entry); got != factsToday {
t.Errorf("claude-code's facts.json fills to\n%s\nwant today's\n%s", got, factsToday)
}
}
// The port the offer serves is the port the manifest serves and the console listens on.
func TestTheOfferServesThePortTheManifestServes(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Serves map[string]map[string]any `json:"serves"`
Moved bool `json:"moved"`
State []string `json:"state"`
Reads []string `json:"reads"`
Provides []struct {
Name string `json:"name"`
AnsweredBy string `json:"answered-by"`
} `json:"provides"`
Resources []struct {
ID string `json:"id"`
} `json:"resources"`
Build struct {
Artifacts []struct {
Name string `json:"name"`
Env map[string]string `json:"env"`
} `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if got := m.Serves[Provision]["port"]; got != float64(ServedPort) {
t.Errorf("the manifest serves %v for %s, the offer %d", got, Provision, ServedPort)
}
if m.Moved {
t.Error("node-tools is marked moved: that also hands the runtime's bus account to the vault (issue 791 round 1)")
}
if len(m.Provides) != 1 || m.Provides[0].Name != Provision || m.Provides[0].AnsweredBy != "helper" {
t.Errorf("node-tools does not say its %s is answered by the helper: %+v", Provision, m.Provides)
}
for _, r := range m.Resources {
if r.ID == "consumers" {
t.Error("the manifest still carries a consumers file the helper does not need")
}
}
for _, r := range m.Reads {
if r == "mesh-controller.assignments" {
t.Error("the manifest still reads mesh-controller.assignments, which the sdk's helper no longer reads (issue 1954)")
}
}
for _, a := range m.Build.Artifacts {
if a.Name == "files" || len(a.Env) > 0 {
t.Errorf("the build still carries %+v, which only a moved module needs", a)
}
}
// Every bucket the sdk's helper reaches is a state the manifest keeps or reads, by the sdk's names: the provider's
// own and the seat mesh-provisioner's provisions and plans (issue 1954 L6-T1); the controller's assignments no more.
declared := map[string]bool{}
for _, n := range append(m.State, m.Reads...) {
declared[n] = true
}
for bucket, want := range map[string]string{
provider.BucketOf(Module): "bound",
provisions.ProvisionsBucket: "mesh-provisioner.provisions",
provisions.PlansBucket: "mesh-provisioner.plans",
} {
got, err := (busOf{}).state(bucket)
if err != nil || got != want || !declared[got] {
t.Errorf("the helper's bucket %s reaches state %q (%v), want %q declared in the manifest", bucket, got, err, want)
}
}
}
// Only the machine's runtime is the provider: a process on another credential, or with no machine, answers nobody.
func TestOnlyTheMachinesRuntimeStartsTheHelper(t *testing.T) {
said := &mt.Logs{}
for _, c := range []struct{ module, node string }{{"claude-code", "anchor"}, {"node-tools", ""}} {
Start(nil, c.module, c.node, time.Second, said.Logf)()
}
}