The module is one Go binary the runtime launches: the renderer (its instruction file held byte for byte to the TypeScript one it replaces), the credentials and identity files, the licence flow of ADR 0206 and the MCP servers in state. Keeps the TypeScript module's key files, so a node moving to it keeps its key. The npm package, its tests and its build go. Both binaries were run together under the real runtime on a test bus with postgres and a stub vendor: a login was adopted by one exchange, the node bound and handed an access token, its file left with no refresh token, and no token in either state.
365 lines
14 KiB
Go
365 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var now = time.Now().UnixMilli()
|
|
|
|
func node(t *testing.T, name string) (Paths, map[string]string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
p := Paths{State: filepath.Join(root, "state"), Facts: filepath.Join(root, "state", "facts.json"),
|
|
Settings: filepath.Join(root, "state", "settings.json"), Home: filepath.Join(root, "home"), Node: name}
|
|
_ = os.MkdirAll(p.State, 0o700)
|
|
_ = os.MkdirAll(filepath.Join(p.Home, ".claude"), 0o700)
|
|
_ = os.WriteFile(p.Facts, []byte(`{"node":"`+name+`","console":"http://127.0.0.1:4270/mcp"}`), 0o600)
|
|
_ = os.WriteFile(p.Settings, []byte(`{"role":"","mcp_servers":{}}`), 0o600)
|
|
return p, map[string]string{}
|
|
}
|
|
|
|
func writer(w map[string]string) WriteManaged {
|
|
return func(name, content string) (string, error) { w[name] = content; return name + ": written", nil }
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func creds(t *testing.T, p Paths) map[string]any {
|
|
t.Helper()
|
|
var c map[string]any
|
|
raw, _ := os.ReadFile(p.credentials())
|
|
if err := json.Unmarshal(raw, &c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c["claudeAiOauth"].(map[string]any)
|
|
}
|
|
|
|
// ---- the renderer, held to the TypeScript it replaced -------------------------------------------------
|
|
|
|
func TestTheRendererWritesWhatTheTypeScriptOneWrote(t *testing.T) {
|
|
raw, err := os.ReadFile("testdata/rendered-by-typescript.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var f struct {
|
|
Facts Facts `json:"facts"`
|
|
Settings Settings `json:"settings"`
|
|
Registered Servers `json:"registered"`
|
|
WithKey map[string]string `json:"withKey"`
|
|
Plain map[string]string `json:"plain"`
|
|
}
|
|
if err := json.Unmarshal(raw, &f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
same := func(label string, got, want map[string]string) {
|
|
if got["CLAUDE.md"] != want["CLAUDE.md"] {
|
|
t.Errorf("%s: CLAUDE.md differs from the TypeScript's:\n--- go\n%s\n--- typescript\n%s", label, got["CLAUDE.md"], want["CLAUDE.md"])
|
|
}
|
|
for _, file := range []string{"managed-mcp.json", "managed-settings.json"} {
|
|
var a, b any
|
|
_ = json.Unmarshal([]byte(got[file]), &a)
|
|
_ = json.Unmarshal([]byte(want[file]), &b)
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Errorf("%s: %s means something else:\n--- go\n%s\n--- typescript\n%s", label, file, got[file], want[file])
|
|
}
|
|
}
|
|
}
|
|
same("with an API key", Render(f.Facts, f.Settings, &Binding{Licence: "api", Kind: "api-key"}, "/state/api-key-helper", f.Registered), f.WithKey)
|
|
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}), f.Plain)
|
|
}
|
|
|
|
func TestASettingCannotReplaceTheMeshsOwnEntryAndABadNameIsLeftOut(t *testing.T) {
|
|
out := Render(Facts{Node: "w", Console: "http://127.0.0.1:4270/mcp"},
|
|
Settings{MCPServers: map[string]map[string]any{"mesh": {"type": "http", "url": "http://evil"}, "bad name": {}}}, nil, "/h", nil)
|
|
var mcp struct {
|
|
MCPServers map[string]map[string]any `json:"mcpServers"`
|
|
}
|
|
_ = json.Unmarshal([]byte(out["managed-mcp.json"]), &mcp)
|
|
if mcp.MCPServers["mesh"]["url"] != "http://127.0.0.1:4270/mcp" || mcp.MCPServers["bad name"] != nil {
|
|
t.Fatalf("%v", mcp.MCPServers)
|
|
}
|
|
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil)) {
|
|
t.Fatal("rendering is not deterministic")
|
|
}
|
|
}
|
|
|
|
// ---- the credentials file -----------------------------------------------------------------------------
|
|
|
|
func i64(v int64) *int64 { return &v }
|
|
|
|
func TestTheLineageRules(t *testing.T) {
|
|
const hour = 3_600_000
|
|
g := func(at string, exp int64, rtExp int64) Grant {
|
|
return Grant{AccessToken: at, ExpiresAt: exp, RefreshTokenExpiresAt: i64(rtExp)}
|
|
}
|
|
month := now + 30*24*hour
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + hour, RefreshTokenExpiresAt: i64(month)}, g("B", now+2*hour, month), false); !d.Apply {
|
|
t.Fatal("a newer rotation was refused")
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "new", ExpiresAt: now + 2*hour, RefreshTokenExpiresAt: i64(month)}, g("old", now+hour, month), false); d.Apply || d.Reason != "not-newer" {
|
|
t.Fatalf("a late older rotation: %+v", d)
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour, RefreshTokenExpiresAt: i64(month)}, g("re", now+hour, now+5*24*hour), false); !d.Apply || !d.Reissued {
|
|
t.Fatalf("a re-issued grant: %+v", d)
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour}, g("other", now+hour, month), true); !d.Apply {
|
|
t.Fatal("a switch was refused")
|
|
}
|
|
if d := DecideApply(&Grant{AccessToken: "A"}, Grant{AccessToken: "A"}, true); d.Apply || d.Reason != "already-current" {
|
|
t.Fatalf("the same token: %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestALoginIsSeenAndStrippedWhenTheNodesOwnGrantIsWritten(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-login","refreshToken":"rt-login","expiresAt":1700000000000},"other":1}`)
|
|
login := ReadCredentials(p.credentials())
|
|
if !HoldsLogin(login) {
|
|
t.Fatal("a login was not seen")
|
|
}
|
|
if err := WriteCredentials(p.credentials(), WithGrant(login, Grant{AccessToken: "at-mesh", ExpiresAt: 1, Scopes: []string{"user:inference"}})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back := ReadCredentials(p.credentials())
|
|
raw, _ := os.ReadFile(p.credentials())
|
|
info, _ := os.Stat(p.credentials())
|
|
if HoldsLogin(back) || GrantOf(back).AccessToken != "at-mesh" || back["other"] == nil || strings.Contains(string(raw), "rt-login") || info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("written %s (mode %v)", raw, info.Mode())
|
|
}
|
|
}
|
|
|
|
func TestTheAccountIsReadFromTheAgentsStateFileAndNeverGuessed(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"},"other":2}`)
|
|
if id := ReadIdentity(p.account()); id == nil || id.AccountUUID != "u-1" || id.EmailAddress != "a@example.org" {
|
|
t.Fatalf("%+v", id)
|
|
}
|
|
if ReadIdentity("/nonexistent/.claude.json") != nil {
|
|
t.Fatal("an identity from nothing")
|
|
}
|
|
writeFile(t, p.account(), `{}`)
|
|
if ReadIdentity(p.account()) != nil {
|
|
t.Fatal("an identity from an empty file")
|
|
}
|
|
}
|
|
|
|
// ---- the licence, ADR 0206 ----------------------------------------------------------------------------
|
|
|
|
func TestWhatANodeHoldsIsReportedWithFingerprintsAndItsAccountNeverAToken(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-secret","refreshToken":"rt-secret","expiresAt":2000,"refreshTokenExpiresAt":9000}}`)
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"}}`)
|
|
h := HoldingsOf(p)
|
|
if h.Node != "laptop" || h.Identity.AccountUUID != "u-1" || *h.Kind != "subscription" || !h.Refresh.Present ||
|
|
!strings.HasPrefix(*h.Refresh.Fingerprint, "sha256:") || h.Access.ExpiresAt != 2000 || h.ChangedAt == nil {
|
|
t.Fatalf("%+v", h)
|
|
}
|
|
raw, _ := json.Marshal(h)
|
|
if strings.Contains(string(raw), "at-secret") || strings.Contains(string(raw), "rt-secret") {
|
|
t.Fatalf("a token is in the report: %s", raw)
|
|
}
|
|
var keys map[string]any
|
|
_ = json.Unmarshal(raw, &keys)
|
|
for k := range keys {
|
|
if strings.Contains(strings.ToLower(k), "token") || strings.Contains(strings.ToLower(k), "secret") {
|
|
t.Fatalf("a field the runtime would refuse: %s", k)
|
|
}
|
|
}
|
|
// The manager reads exactly this shape.
|
|
if _, err := time.Parse(time.RFC3339Nano, *h.ChangedAt); err != nil {
|
|
t.Fatalf("the manager cannot read the report's time: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheGrantAnswersOnlyAWaitingLoginSealedToTheManagersKey(t *testing.T) {
|
|
p, _ := node(t, "laptop")
|
|
manager, _ := GenerateKeyPair()
|
|
if a, _ := GrantFor(p, manager.PublicKey); a.Sealed != nil || a.Waiting == nil || *a.Waiting {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at","refreshToken":"rt-login","expiresAt":1}}`)
|
|
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-9"}}`)
|
|
a, err := GrantFor(p, manager.PublicKey)
|
|
if err != nil || a.Identity.AccountUUID != "u-9" {
|
|
t.Fatalf("%+v %v", a, err)
|
|
}
|
|
plain, _ := Open(*a.Sealed, manager.PrivateKey)
|
|
if !strings.Contains(plain, `"refreshToken":"rt-login"`) {
|
|
t.Fatalf("opened %s", plain)
|
|
}
|
|
raw, _ := json.Marshal(a)
|
|
if strings.Contains(string(raw), "rt-login") {
|
|
t.Fatal("the refresh token crossed in the clear")
|
|
}
|
|
}
|
|
|
|
// seat answers `current` as the manager does: the grant sealed to the key the node sent.
|
|
func seat(t *testing.T, licence, token string, gen int64, asked *[]string) Ask {
|
|
return func(address string, args any) (json.RawMessage, error) {
|
|
*asked = append(*asked, address)
|
|
key := args.(map[string]any)["public_key"].(string)
|
|
g, _ := json.Marshal(Grant{AccessToken: token, ExpiresAt: now + 3_600_000})
|
|
box, err := Seal(string(g), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return json.Marshal(Current{Licence: licence, Kind: "subscription", Generation: gen, Sealed: &box})
|
|
}
|
|
}
|
|
|
|
func TestANewerGenerationFetchesTheTokenOnceByTheSeatsVerb(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
var asked []string
|
|
ask := seat(t, "personal", "at-1", 3, &asked)
|
|
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(asked) != 1 || asked[0] != "seat:anthropic-licence-manager.current" || creds(t, p)["accessToken"] != "at-1" {
|
|
t.Fatalf("asked %v, credentials %v", asked, creds(t, p))
|
|
}
|
|
if done, _ := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); done != "" || len(asked) != 1 {
|
|
t.Fatal("an equal generation asked again")
|
|
}
|
|
if HoldingsOf(p).Generation != 3 || w["managed-mcp.json"] == "" {
|
|
t.Fatal("the generation or the managed files were not written")
|
|
}
|
|
}
|
|
|
|
func TestTheTokenANodeIsHandedReplacesALoginsGrantAndLeavesNoRefreshToken(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-old","refreshToken":"rt-spent","expiresAt":`+
|
|
strings.TrimSpace(string(mustJSON(now+7_200_000)))+`}}`)
|
|
var asked []string
|
|
out, err := Pull(p, seat(t, "personal", "at-new", 1, &asked), writer(w))
|
|
if err != nil || out["applied"] != true {
|
|
t.Fatalf("%v %v", out, err)
|
|
}
|
|
c := creds(t, p)
|
|
if c["accessToken"] != "at-new" || c["refreshToken"] != nil || HoldingsOf(p).Refresh.Present {
|
|
t.Fatalf("%v", c)
|
|
}
|
|
}
|
|
|
|
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
|
|
|
|
// ---- MCP servers in state, ADR 0201 -------------------------------------------------------------------
|
|
|
|
// bus is the `servers` state as every node in a test shares it, with each node's watch.
|
|
type bus struct {
|
|
kept map[string]map[string]any
|
|
watchers []func(ServerChange)
|
|
}
|
|
|
|
func (b *bus) Put(key string, value any) error {
|
|
v := value.(map[string]any)
|
|
b.kept[key] = v
|
|
for _, w := range b.watchers {
|
|
w(ServerChange{Key: key, Op: "put", Value: v})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bus) Delete(key string) error {
|
|
delete(b.kept, key)
|
|
for _, w := range b.watchers {
|
|
w(ServerChange{Key: key, Op: "delete"})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *bus) Keys() ([]string, error) {
|
|
var out []string
|
|
for k := range b.kept {
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// join is a node joining: its view takes the current state, then every change.
|
|
func (b *bus) join(p Paths, w map[string]string) *ServerView {
|
|
v := NewServerView(p)
|
|
for k, val := range b.kept {
|
|
_, _ = OnServerChange(v, ServerChange{Key: k, Op: "put", Value: val}, p, writer(w))
|
|
}
|
|
b.watchers = append(b.watchers, func(c ServerChange) { _, _ = OnServerChange(v, c, p, writer(w)) })
|
|
return v
|
|
}
|
|
|
|
func noOthers() ([]string, error) { return nil, nil }
|
|
|
|
func TestRegisteringHerePutsItUnderThisNodesKeyAndAsksAboutTheOthers(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
v := b.join(p, w)
|
|
r, err := RegisterServer(p, Registration{Name: "search", Entry: map[string]any{"type": "http", "url": "https://s.example/mcp"}}, b, v, writer(w),
|
|
func() ([]string, error) { return []string{"laptop", "server", "desktop"}, nil })
|
|
if err != nil || r["here"] != "changed" || !strings.Contains(r["also"].(string), "server, desktop") || b.kept["laptop.search"] == nil {
|
|
t.Fatalf("%v %v %v", r, err, b.kept)
|
|
}
|
|
if !strings.Contains(w["managed-mcp.json"], `"search"`) {
|
|
t.Fatal("not rendered")
|
|
}
|
|
}
|
|
|
|
func TestEveryNodeRegistrationReachesTheOthersAndALateNodeReadsIt(t *testing.T) {
|
|
a, wa := node(t, "laptop")
|
|
s, ws := node(t, "server")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
va := b.join(a, wa)
|
|
b.join(s, ws)
|
|
_, _ = RegisterServer(a, Registration{Name: "docs", Entry: map[string]any{"type": "stdio", "command": "docs-mcp"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
if Registered(s)["docs"] == nil || !strings.Contains(ws["managed-mcp.json"], "docs-mcp") {
|
|
t.Fatalf("the other node did not take it: %v", Registered(s))
|
|
}
|
|
late, wl := node(t, "desktop")
|
|
b.join(late, wl)
|
|
if Registered(late)["docs"] == nil {
|
|
t.Fatal("a node joining later did not read the current set")
|
|
}
|
|
_, _ = RegisterServer(a, Registration{Name: "docs", Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
if Registered(s)["docs"] != nil || Registered(late)["docs"] != nil {
|
|
t.Fatal("an unregistration did not reach every node")
|
|
}
|
|
}
|
|
|
|
func TestANodesOwnRegistrationOverridesTheOneForEveryNode(t *testing.T) {
|
|
a, wa := node(t, "laptop")
|
|
s, ws := node(t, "server")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
va := b.join(a, wa)
|
|
b.join(s, ws)
|
|
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://all"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
|
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://laptop"}}, b, va, writer(wa), noOthers)
|
|
if Registered(a)["x"]["url"] != "https://laptop" || Registered(s)["x"]["url"] != "https://all" {
|
|
t.Fatalf("%v %v", Registered(a), Registered(s))
|
|
}
|
|
r, _ := RegisterServer(a, Registration{Name: "x"}, b, va, writer(wa), noOthers)
|
|
if !strings.Contains(r["still"].(string), "still applies here") || Registered(a)["x"]["url"] != "https://all" {
|
|
t.Fatalf("%v", r)
|
|
}
|
|
}
|
|
|
|
func TestABadEntryIsRefusedBeforeAnythingIsPut(t *testing.T) {
|
|
p, w := node(t, "laptop")
|
|
b := &bus{kept: map[string]map[string]any{}}
|
|
v := b.join(p, w)
|
|
r, _ := RegisterServer(p, Registration{Name: "mesh", Entry: map[string]any{"type": "http", "url": "https://x"}}, b, v, writer(w), noOthers)
|
|
if r["registered"] != false || len(b.kept) != 0 {
|
|
t.Fatalf("%v %v", r, b.kept)
|
|
}
|
|
if done, _ := OnServerChange(v, ServerChange{Key: "server.b", Op: "put", Value: map[string]any{"type": "http", "url": "https://b"}}, p, writer(w)); done != "" {
|
|
t.Fatal("another node's key changed this one")
|
|
}
|
|
}
|