Files
mesh-catalog/modules/claude-code/cmd/claude-code/claude_code_test.go
T
jschoubben 31ce47d1cc C-3: a node takes its licence token from the vault (issue 1409)
The agent module asks the seat's current which licence it holds and its key,
then the vault's get for that licence's access secret sealed to its own key:
at its start, on a binding.changed with a newer generation than it applied,
and on the vault's secret-changed for the licence it holds. The applied
binding keeps the licence's key, so the vault's event is matched by the
carried key, never one formed here.

Builds: design 39 bus-and-switch §19, §18, B7, B9, B11

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
2026-11-11 23:51:00 +01:00

609 lines
25 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"slices"
"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{}
}
// confinedNode is a node whose agent runs as an account of its own (novox/hq ADR 0266), its home that account's:
// the one kind of node the home scope writes into (novox/hq issue 340).
func confinedNode(t *testing.T, name string) (Paths, map[string]string) {
t.Helper()
p, w := node(t, name)
p.Agent, p.Operator, p.AgentHome, p.OperatorHome = "agent", "operator", p.Home, filepath.Join(t.TempDir(), "operator")
return p, w
}
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)
// The plugin's two keys are new since the TypeScript (ADR 0216); everything else means the same.
if m, ok := a.(map[string]any); ok && file == "managed-settings.json" {
for k := range MarketplaceKeys() {
delete(m, k)
}
// The wait bound is new since the TypeScript (issue 1871), and is held below.
if env, ok := m["env"].(map[string]any); ok {
delete(env, "CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT")
if len(env) == 0 {
delete(m, "env")
}
}
}
if m, ok := a.(map[string]any); ok && file == "managed-mcp.json" {
if servers, ok := m["mcpServers"].(map[string]any); ok {
if entry, ok := servers["mesh"].(map[string]any); ok {
delete(entry, "timeout")
}
}
}
_ = 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, Config{}), f.WithKey)
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}, Config{}), 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, Config{})
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)
}
// The entry bounds a call by mesh_wait's longest wait and the idle bound is laid over the operator's env
// (issue 1871): a session's wait is not cut at the agent's one-minute default.
if mcp.MCPServers["mesh"]["timeout"] != float64(660000) {
t.Errorf("the mesh entry's timeout is %v", mcp.MCPServers["mesh"]["timeout"])
}
for _, held := range []map[string]any{nil, {"CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT": "1", "FOO": "bar"}} {
var m struct {
Env map[string]any `json:"env"`
}
got := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{"env": held}}, nil, "/h", nil, Config{})
_ = json.Unmarshal([]byte(got["managed-settings.json"]), &m)
if m.Env["CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT"] != "660000" || (held != nil && m.Env["FOO"] != "bar") {
t.Errorf("env %v rendered as %v", held, m.Env)
}
}
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil, Config{}), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil, Config{})) {
t.Fatal("rendering is not deterministic")
}
}
func TestTheOperatorsManagedSettingsAreLaidUnderTheMeshsOwnKeys(t *testing.T) {
settings := Settings{ManagedSettings: map[string]any{
"autoMode": map[string]any{"allow": []any{"merging an approved pull request"}},
"permissions": map[string]any{"deny": []any{"Bash(rm -rf /)"}},
"attribution": map[string]any{"commit": "made by a machine"},
"allowAllClaudeAiMcps": false,
"apiKeyHelper": "/somewhere/else",
}}
read := func(binding *Binding) map[string]any {
var m map[string]any
out := Render(Facts{Console: "x"}, settings, binding, "/h", nil, Config{})
if err := json.Unmarshal([]byte(out["managed-settings.json"]), &m); err != nil {
t.Fatal(err)
}
return m
}
m := read(nil)
if allow := m["autoMode"].(map[string]any)["allow"].([]any); len(allow) != 1 || allow[0] != "merging an approved pull request" {
t.Errorf("the operator's auto mode was not carried: %v", m["autoMode"])
}
if m["permissions"] == nil {
t.Errorf("the operator's permissions were not carried: %v", m)
}
if !reflect.DeepEqual(m["attribution"], map[string]any{"commit": "", "pr": ""}) || m["allowAllClaudeAiMcps"] != true {
t.Errorf("a setting replaced the mesh's own keys: %v", m)
}
if _, ok := m["apiKeyHelper"]; ok {
t.Errorf("a setting named a key-helper the licence did not: %v", m)
}
if helper := read(&Binding{Licence: "api", Kind: "api-key"})["apiKeyHelper"]; helper != "/h" {
t.Errorf("an API-key licence's key-helper was replaced: %v", helper)
}
}
// ---- 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 licence and its key and never a token, and `get` as the vault does:
// the licence's access secret sealed to the key the node sent (design 39 bus-and-switch §18, §19).
func seat(t *testing.T, licence, token string, gen int64, asked *[]string) Ask {
return vaulted(t, Current{Licence: licence, Key: licence, Kind: "subscription", Generation: gen}, token, asked)
}
// vaulted answers `current` with c and the vault's `get` of c's access secret with token.
func vaulted(t *testing.T, c Current, token string, asked *[]string) Ask {
return func(address string, args any) (json.RawMessage, error) {
*asked = append(*asked, address)
a := args.(map[string]any)
switch address {
case SeatVerb("current"):
if _, sent := a["public_key"]; sent || a["consumer"] == nil {
t.Fatalf("current asked with %v", a)
}
return json.Marshal(c)
case VaultGet:
if a["name"] != AccessSecret(c.Key) {
t.Fatalf("the vault asked for %v", a["name"])
}
// Each answer a little later, as each rotation hands a later token.
g, _ := json.Marshal(Grant{AccessToken: token, ExpiresAt: now + 3_600_000 + int64(len(*asked))*1000})
box, err := Seal(string(g), a["public_key"].(string))
if err != nil {
t.Fatal(err)
}
return json.Marshal(map[string]any{"name": a["name"], "generation": 1, "sealed": box})
}
t.Fatalf("asked %s", address)
return nil, nil
}
}
// design 39 bus-and-switch §19: a newer generation asks the seat which licence, and the vault for its token.
func TestANewerGenerationFetchesTheTokenOnceFromTheVault(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) != 2 || asked[0] != SeatVerb("current") || asked[1] != VaultGet || 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) != 2 {
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")
}
}
// The node's line names the generation it applied and why the manager moved it, and who asked (novox/hq issue 625).
func TestAnAppliedBindingSaysWhyAndWho(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
done, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 7,
Why: "the switch verb", By: "g14.node-tools"}, seat(t, "personal", "at-7", 7, &asked), writer(w))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`"generation":7`, `"why":"the switch verb"`, `"by":"g14.node-tools"`} {
if !strings.Contains(done, want) {
t.Fatalf("the line %s lacks %s", done, want)
}
}
if strings.Contains(done, "at-7") {
t.Fatal("the line carries the token")
}
}
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) Get(key string) (json.RawMessage, bool, error) {
v, ok := b.kept[key]
if !ok {
return nil, false, nil
}
raw, err := json.Marshal(v)
return raw, true, err
}
// existing is a server registered before registering was refused (novox/hq issue 340), as the state still holds it.
func (b *bus) existing(key string, entry map[string]any) { _ = b.Put(key, entry) }
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 }
// A server registered before stays (novox/hq issue 340): every node takes what the state holds for it, a node
// that joins later reads the current set, and an unregistration reaches every node.
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)
b.existing("all.docs", map[string]any{"type": "stdio", "command": "docs-mcp"})
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)
b.existing("all.x", map[string]any{"type": "http", "url": "https://all"})
b.existing("laptop.x", map[string]any{"type": "http", "url": "https://laptop"})
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 TestABadEntryIsNeverTaken(t *testing.T) {
p, w := node(t, "laptop")
b := &bus{kept: map[string]map[string]any{}}
v := b.join(p, w)
b.existing("laptop.mesh", map[string]any{"type": "http", "url": "https://x"})
if Registered(p)["mesh"] != nil || strings.Contains(w["managed-mcp.json"], "https://x") {
t.Fatalf("the mesh's own entry was replaced: %v", Registered(p))
}
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")
}
}
// An API key goes to the manager sealed to its key, never in the clear, and its file is gone afterwards.
func TestAnAPIKeyIsHandedOverSealedAndItsFileRemoved(t *testing.T) {
p, _ := node(t, "laptop")
manager, _ := GenerateKeyPair()
file := filepath.Join(p.Home, "api-key")
writeFile(t, file, "sk-ant-api03-secret-0123456789abcdefghij\n")
var sent []string
ask := func(address string, args any) (json.RawMessage, error) {
raw, _ := json.Marshal(args)
sent = append(sent, address+" "+string(raw))
switch address {
case "seat:anthropic-licence-manager.public-key":
return json.Marshal(map[string]any{"public_key": manager.PublicKey})
case "seat:anthropic-licence-manager.adopt":
box := args.(map[string]any)["sealed"].(SealedBox)
if key, err := Open(box, manager.PrivateKey); err != nil || key != "sk-ant-api03-secret-0123456789abcdefghij" {
t.Fatalf("the manager opened %q, %v", key, err)
}
return json.Marshal(map[string]any{"adopted": true})
case "seat:anthropic-licence-manager.switch":
return json.Marshal(map[string]any{"licence": "api"})
}
t.Fatalf("asked %s", address)
return nil, nil
}
out, err := AddAPIKey(p, "api", file, true, ask)
if err != nil || out["file"] != "removed" || out["switched"] == nil {
t.Fatalf("%v %v", out, err)
}
if _, err := os.Stat(file); !os.IsNotExist(err) {
t.Fatal("the key file is still there")
}
if strings.Contains(strings.Join(sent, "\n"), "sk-ant") {
t.Fatalf("the key crossed in the clear: %v", sent)
}
}
// design 39 bus-and-switch §19, B11: only a newer generation than the one applied is fetched; a lower one is not, and
// the vault's secret-changed for the licence held asks its token again, for another secret nothing.
func TestTheTokenIsAskedOnANewerGenerationAndOnTheVaultsChange(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 16},
seat(t, "personal", "at-old", 16, &asked), writer(w)); err != nil {
t.Fatal(err)
}
if done, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3},
seat(t, "personal", "at-low", 3, &asked), writer(w)); err != nil || done != "" || len(asked) != 2 {
t.Fatalf("a lower generation was fetched: %q %v, asked %v", done, err, asked)
}
rotated := seat(t, "personal", "at-new", 16, &asked)
for _, other := range []string{`{"name":"licence.personal.grant","generation":5}`, `{"name":"licence.other.access","generation":5}`} {
if done, err := OnSecretChanged(p, []byte(other), rotated, writer(w)); err != nil || done != "" || len(asked) != 2 {
t.Fatalf("%s asked again: %q %v, asked %v", other, done, err, asked)
}
}
if _, err := OnSecretChanged(p, []byte(`{"name":"licence.personal.access","generation":5,"by":"x","at":"2026-11-11T00:00:00Z"}`),
rotated, writer(w)); err != nil {
t.Fatal(err)
}
if len(asked) != 4 || asked[3] != VaultGet || creds(t, p)["accessToken"] != "at-new" || HoldingsOf(p).Generation != 16 {
t.Fatalf("asked %v, credentials %v: the vault's change was not applied", asked, creds(t, p))
}
}
// design 39 bus-and-switch §22/AC1, B22: binding.changed, decoded from a copy of the manager's form, is acted on only
// when it names this node's consumer; an event for another consumer is ignored.
func TestAnEventForAnotherConsumerIsIgnored(t *testing.T) {
p, w := node(t, "server")
var asked []string
ask := seat(t, "api", "at-3", 3, &asked)
said, err := os.ReadFile("testdata/binding-changed.json")
if err != nil {
t.Fatal(err)
}
if done, err := OnBindingChanged(p, said, ask, writer(w)); err != nil || done != "" || len(asked) != 0 {
t.Fatalf("an event for laptop was acted on by server: %q %v, asked %v", done, err, asked)
}
p, w = node(t, "laptop")
if done, err := OnBindingChanged(p, said, ask, writer(w)); err != nil || !strings.Contains(done, `"generation":3`) || len(asked) != 2 {
t.Fatalf("laptop's own event: %q %v, asked %v", done, err, asked)
}
if done, _ := OnBindingChanged(p, []byte(`{"consumer":"laptop","licence":"","generation":3,"from":"api"}`), ask, writer(w)); !strings.Contains(done, "released") {
t.Fatalf("a release said %q", done)
}
}
// design 39 bus-and-switch §22, B10: at its start the module asks the seat's bindings for its own consumer and applies
// that first view; its manifest consumes binding.changed and no longer reads the manager's state.
func TestTheFirstViewOfTheBindingIsApplied(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
current := vaulted(t, Current{Licence: "api", Key: "api", Kind: "subscription", Generation: 9}, "at-9", &asked)
ask := func(address string, args any) (json.RawMessage, error) {
if address == SeatVerb("bindings") {
asked = append(asked, address)
if args.(map[string]any)["consumer"] != "laptop" {
t.Fatalf("bindings asked with %v", args)
}
return json.RawMessage(`[{"consumer":"laptop","licence":"api","key":"api","kind":"api-key","generation":9,"why":"carried over","by":"claude-licence-manager"}]`), nil
}
return current(address, args)
}
if done, err := FirstBinding(p, ask, writer(w)); err != nil || !strings.Contains(done, `"generation":9`) {
t.Fatalf("the first view: %q %v, asked %v", done, err, asked)
}
if len(asked) != 3 || asked[0] != SeatVerb("bindings") || creds(t, p)["accessToken"] != "at-9" {
t.Fatalf("asked %v, credentials %v", asked, creds(t, p))
}
// §19: at every start the vault is asked, though the binding is the one applied.
if _, err := FirstBinding(p, ask, writer(w)); err != nil || len(asked) != 6 || asked[5] != VaultGet {
t.Fatalf("a start asked %v", asked)
}
raw, _ := os.ReadFile("../../module.json")
var m struct {
Consumes, Invokes []string
Reads []string
}
_ = json.Unmarshal(raw, &m)
if !slices.Contains(m.Consumes, BindingChanged) || !slices.Contains(m.Consumes, SecretChanged) ||
!slices.Contains(m.Invokes, SeatVerb("bindings")) || !slices.Contains(m.Invokes, VaultGet) || len(m.Reads) != 0 {
t.Fatalf("the manifest consumes %v, invokes %v, reads %v", m.Consumes, m.Invokes, m.Reads)
}
}