Files
mesh-catalog/modules/claude-licence-manager/cmd/manager/busstore_test.go
T
jschoubben cff1005422
mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
P2: the licence manager is four parts of one manifest (issue 1409)
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and
the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they
answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept
data and the log entries, the key form, the entries' names and the settings with their validity; the settings file
and the account with its directory stay as they are.

Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
2026-11-12 12:08:03 +01:00

362 lines
11 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// memState is a state of the bus as the runtime answers it, in memory: values, their past values, keys' lifetimes.
type memState struct {
mu sync.Mutex
now func() time.Time
values map[string]json.RawMessage
until map[string]time.Time
history map[string][]stdio.StateRevision
rev uint64
writes int
}
func newMemState(now func() time.Time) *memState {
return &memState{now: now, values: map[string]json.RawMessage{}, until: map[string]time.Time{},
history: map[string][]stdio.StateRevision{}}
}
func (s *memState) live(key string) bool {
if u, ok := s.until[key]; ok && !s.now().Before(u) {
delete(s.values, key)
delete(s.until, key)
}
_, ok := s.values[key]
return ok
}
func (s *memState) Get(key string) (*stdio.StateEntry, error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.live(key) {
return nil, nil
}
return &stdio.StateEntry{Key: key, Value: s.values[key]}, nil
}
func (s *memState) write(key string, value any, ttl time.Duration, create bool) (uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
if create && s.live(key) {
return 0, stdio.ErrChanged
}
raw, err := json.Marshal(value)
if err != nil {
return 0, err
}
s.rev++
s.writes++
s.values[key] = raw
delete(s.until, key)
if ttl > 0 {
s.until[key] = s.now().Add(ttl)
}
s.history[key] = append(s.history[key], stdio.StateRevision{Key: key, Value: raw, Revision: s.rev, Op: "put", At: s.now()})
return s.rev, nil
}
func (s *memState) Put(key string, value any) (uint64, error) { return s.write(key, value, 0, false) }
func (s *memState) PutTTL(key string, value any, ttl time.Duration) (uint64, error) {
return s.write(key, value, ttl, false)
}
func (s *memState) Create(key string, value any) (uint64, error) { return s.write(key, value, 0, true) }
func (s *memState) CreateTTL(key string, value any, ttl time.Duration) (uint64, error) {
return s.write(key, value, ttl, true)
}
func (s *memState) Delete(key string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.rev++
s.writes++
delete(s.values, key)
delete(s.until, key)
s.history[key] = append(s.history[key], stdio.StateRevision{Key: key, Revision: s.rev, Op: "delete", At: s.now()})
return nil
}
func (s *memState) Keys() ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := []string{}
for k := range s.values {
if s.live(k) {
out = append(out, k)
}
}
sort.Strings(out)
return out, nil
}
func (s *memState) History(key string) ([]stdio.StateRevision, error) {
s.mu.Lock()
defer s.mu.Unlock()
return append([]stdio.StateRevision(nil), s.history[key]...), nil
}
// design 39 bus-and-switch §2: a rotation and a reading each write the licence's key, by its key form, and no field
// holds a token.
func TestARotationAndAReadingWriteTheLicenceKey(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
key := KeyFor(licence1)
before := len(mm.licences.history[key])
if _, err := mm.m.Rotate(ctx, licence1, true); err != nil {
t.Fatal(err)
}
if err := mm.m.ReadUsage(ctx); err != nil {
t.Fatal(err)
}
if got := len(mm.licences.history[key]); got < before+2 {
t.Fatalf("the key %s was written %d times by a rotation and a reading, want 2", key, got-before)
}
for _, r := range mm.licences.history[key] {
for _, token := range []string{"rt-", "at-"} {
if strings.Contains(string(r.Value), token) {
t.Fatalf("a value of %s holds %s: %s", key, token, r.Value)
}
}
}
var l Licence
_ = json.Unmarshal(mm.licences.values[key], &l)
if l.Name != licence1 || l.AccountUUID != "acct-1" || !strings.Contains(string(mm.licences.values[key]), `"account":"acct-1"`) {
t.Fatalf("the key holds %+v", l)
}
}
// design 39 bus-and-switch §3, B11: bind, switch, release and a rotation each write or delete the consumer's key with
// its why and by; each consumer counts its own generations, past what its node reported.
func TestEachMoveWritesTheBindingWithWhyAndBy(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
_ = mm.store.SaveLicence(ctx, Licence{Name: "api", Kind: "api-key", Generation: 1})
mm.m.Reported = func(node string) int64 {
if node == "server" {
return 40
}
return 0
}
asked := WithCause(ctx, Cause{Why: "the switch verb", By: "jane"})
if _, err := mm.m.Bind(asked, "server", "api"); err != nil {
t.Fatal(err)
}
if b := mm.bound("server"); b.Licence != "api" || b.Generation != 41 || b.Why != "the switch verb" || b.By != "jane" ||
b.Since == "" || b.Kind != "api-key" {
t.Fatalf("server's binding %+v", b)
}
laptop := mm.bound("laptop").Generation
if _, err := mm.m.Bind(asked, "laptop", "api"); err != nil {
t.Fatal(err)
}
if b := mm.bound("laptop"); b.Generation != laptop+1 || b.Licence != "api" {
t.Fatalf("laptop's switch %+v, from generation %d", b, laptop)
}
if _, err := mm.m.Release(WithCause(ctx, Cause{Why: "the release verb", By: "jane"}), "server"); err != nil {
t.Fatal(err)
}
if _, ok := mm.bindings.values["server"]; ok {
t.Fatal("release left the key")
}
if h, _ := mm.bindings.History("server"); len(h) != 2 || h[1].Op != "delete" {
t.Fatalf("server's history %v", h)
}
}
// design 39 bus-and-switch §4, §5, B13: a lease is a key created with a two-minute lifetime that another holder cannot
// create; an offered grant's outcome is kept under its fingerprint in the key form for 90 days.
func TestALeaseAndAnOfferAreKeptByTheirKeysAndLifetimes(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
key := "licence." + KeyFor(licence1)
if ok, err := mm.store.Lease(ctx, key, "one", leaseFor); !ok || err != nil {
t.Fatalf("the first lease: %v %v", ok, err)
}
if ok, _ := mm.store.Lease(ctx, key, "two", leaseFor); ok {
t.Fatal("a second holder took a live lease")
}
if u := mm.leases.until[key]; !u.Equal(mm.now.Add(2 * time.Minute)) {
t.Fatalf("the lease lives until %v", u)
}
mm.now = mm.now.Add(2 * time.Minute)
if ok, _ := mm.store.Lease(ctx, key, "two", leaseFor); !ok {
t.Fatal("a lease past its lifetime was not taken")
}
_ = mm.store.Unlease(ctx, key, "one")
if _, held := mm.leases.values[key]; !held {
t.Fatal("another holder's lease was ended")
}
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-dead", false, t0)})
fp := OfferedKey(Fingerprint("rt-dead"))
if strings.ContainsAny(fp, ":") || !strings.HasPrefix(fp, "sha256_") {
t.Fatalf("the offered key %q is not in the key form", fp)
}
var v OfferedValue
_ = json.Unmarshal(mm.offered.values[fp], &v)
if v.Outcome != Dead || v.Node != "laptop" || v.Account != "acct-1" || v.Why == "" || v.At == "" {
t.Fatalf("offered %+v", v)
}
if u := mm.offered.until[fp]; !u.Equal(mm.now.Add(90 * 24 * time.Hour)) {
t.Fatalf("the outcome lives until %v", u)
}
}
// memLog is a log of the bus as the runtime answers it, in memory; full, it refuses an entry.
type memLog struct {
mu sync.Mutex
now func() time.Time
entries []stdio.LogEntry
full bool
}
func newMemLog(now func() time.Time) *memLog { return &memLog{now: now} }
func (l *memLog) Append(key string, value any) (uint64, error) {
l.mu.Lock()
defer l.mu.Unlock()
if l.full {
return 0, errors.New("the log is full: maximum bytes exceeded")
}
raw, err := json.Marshal(value)
if err != nil {
return 0, err
}
seq := uint64(len(l.entries) + 1)
l.entries = append(l.entries, stdio.LogEntry{Sequence: seq, Key: key, Value: raw, At: l.now()})
return seq, nil
}
func (l *memLog) Read(from uint64, keys string, limit int) (*stdio.LogPage, error) {
l.mu.Lock()
defer l.mu.Unlock()
p := &stdio.LogPage{Entries: []stdio.LogEntry{}, Last: uint64(len(l.entries))}
for _, e := range l.entries {
if e.Sequence >= from && (keys == "" || e.Key == keys) {
if limit > 0 && len(p.Entries) == limit {
p.Next = e.Sequence
break
}
p.Entries = append(p.Entries, e)
}
}
return p, nil
}
func (l *memLog) Last(key string) (*stdio.LogEntry, error) {
l.mu.Lock()
defer l.mu.Unlock()
for i := len(l.entries) - 1; i >= 0; i-- {
if l.entries[i].Key == key {
e := l.entries[i]
return &e, nil
}
}
return nil, nil
}
func (l *memLog) Info() (*stdio.LogInfo, error) {
l.mu.Lock()
defer l.mu.Unlock()
i := &stdio.LogInfo{Entries: uint64(len(l.entries)), Last: uint64(len(l.entries))}
if len(l.entries) > 0 {
i.First = 1
}
return i, nil
}
func (l *memLog) Newest(keys string) ([]stdio.LogEntry, error) {
l.mu.Lock()
defer l.mu.Unlock()
last := map[string]int{}
for i, e := range l.entries {
if keys == "" || e.Key == keys {
last[e.Key] = i
}
}
out := []stdio.LogEntry{}
for i, e := range l.entries {
if j, ok := last[e.Key]; ok && j == i {
out = append(out, e)
}
}
return out, nil
}
// under is every entry of a log's subject, decoded.
func (l *memLog) under(key string) []map[string]any {
l.mu.Lock()
defer l.mu.Unlock()
var out []map[string]any
for _, e := range l.entries {
if key == "" || e.Key == key {
var v map[string]any
_ = json.Unmarshal(e.Value, &v)
v["what"] = e.Key
out = append(out, v)
}
}
return out
}
// design 39 bus-and-switch §6: a reading appends one entry under its licence's key, the reading beside the raw answer.
func TestAReadingAppendsOneUsageEntry(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
if err := mm.m.ReadUsage(ctx); err != nil {
t.Fatal(err)
}
got := mm.usage.under(KeyFor(licence1))
if len(got) != 1 || len(mm.usage.entries) != 1 {
t.Fatalf("a reading appended %v", mm.usage.entries)
}
if got[0]["raw"] == nil || got[0]["reading"] == nil || got[0]["at"] == nil {
t.Fatalf("the entry %v", got[0])
}
rows, _ := mm.store.Usage(ctx, licence1, time.Time{}, 20, false)
if len(rows) != 1 || rows[0].Licence != licence1 || rows[0].Reading.SessionPct == nil {
t.Fatalf("usage answers %+v", rows)
}
}
// design 39 bus-and-switch §7, B20: each act appends one entry under its kind; a full log fails the act loudly.
func TestEachActAppendsOneAuditEntry(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
_, _ = mm.m.Rotate(ctx, licence1, true)
_ = mm.store.SaveLicence(ctx, Licence{Name: "api", Kind: "api-key", Generation: 1})
_, _ = mm.m.Bind(ctx, "laptop", "api")
_, _ = mm.m.Release(ctx, "laptop")
for kind, n := range map[string]int{"adopted": 1, "bound": 1, "rotated": 1, "switched": 1, "released": 1} {
if got := len(mm.audit.under(kind)); got != n {
t.Errorf("%s: %d entries, want %d", kind, got, n)
}
}
for _, e := range mm.audit.under("") {
if e["at"] == nil || e["by"] == nil && e["what"] != "adopted" {
t.Errorf("the entry %v has no at or by", e)
}
}
mm.audit.full = true
if _, err := mm.m.Bind(ctx, "laptop", "api"); err == nil || !strings.Contains(err.Error(), "the audit did not take") {
t.Fatalf("a bind with a full audit log: %v", err)
}
}