Files
mesh-catalog/modules/claude-licence-manager/cmd/manager/switching_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

194 lines
7.8 KiB
Go

package main
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
// read is a licence's latest reading, read at t0.
func read(session, weekly float64) *UsageReading {
return &UsageReading{SessionPct: &session, SessionResetsAt: "2026-11-11T23:00:00Z", WeeklyPct: &weekly,
WeeklyResetsAt: "2026-11-14T09:00:00Z", ReadAt: t0.UTC().Format(time.RFC3339Nano)}
}
// weekly is a reading with no session window, as the vendor answers since the session limit was retired (C4).
func weekly(pct float64) *UsageReading {
u := read(0, pct)
u.SessionPct, u.SessionResetsAt = nil, ""
return u
}
func sub(name string, u *UsageReading) Licence {
return Licence{Name: name, Kind: "subscription", Generation: 1, Usage: u}
}
// design 39 bus-and-switch §26/AC1, §25, §48, C4: a table of licences, readings and bindings and the moves each gives.
func TestTheRuleMovesEachTable(t *testing.T) {
mm := newMesh(t)
on := func(consumers ...string) []Binding {
var out []Binding
for _, c := range consumers {
out = append(out, Binding{Consumer: c, Licence: "a"})
}
return out
}
for _, c := range []struct {
name string
licences []Licence
bindings []Binding
to string
window string
noTarget bool
consumers int
}{
{"below its threshold", []Licence{sub("a", read(50, 98)), sub("b", read(0, 0))}, on("x"), "", "", false, 0},
{"no consumer", []Licence{sub("a", read(99, 99)), sub("b", read(0, 0))}, nil, "", "", false, 0},
{"the weekly window at 99", []Licence{sub("a", read(40, 99)), sub("b", read(10, 30)), sub("c", read(5, 20))},
on("x", "y", "z", "w"), "c", "weekly", false, 4},
{"the session window at 100 moves nothing (C4)", []Licence{sub("a", read(100, 50)), sub("b", read(10, 30))},
on("x"), "", "", false, 0},
{"no session window, weekly 99, to weekly 33 (C4)", []Licence{sub("a", weekly(99)), sub("b", weekly(33))},
on("x", "y"), "b", "weekly", false, 2},
{"a tie goes to the first by name", []Licence{sub("a", read(0, 99)), sub("c", read(10, 10)), sub("b", read(10, 10))},
on("x"), "b", "weekly", false, 1},
{"a target's weekly below room_below_pct, its session not counted (C4)", []Licence{sub("a", read(0, 99)),
sub("b", read(0, 90)), sub("c", read(95, 89))}, on("x"), "c", "weekly", false, 1},
{"no other licence has room", []Licence{sub("a", read(0, 99)), sub("b", read(0, 95)),
{Name: "k", Kind: "api-key", Generation: 1}}, on("x"), "", "", true, 0},
{"an unread licence is no target", []Licence{sub("a", read(0, 99)), sub("b", nil)}, on("x"), "", "", true, 0},
} {
r := mm.m.RuleFor(c.licences[0], c.licences, c.bindings, Defaults)
to, window, n := "", "", 0
if r.Switch != nil {
to, window, n = r.Switch.To, r.Switch.Window, len(r.Switch.Consumers)
}
if to != c.to || window != c.window || n != c.consumers || r.NoTarget != c.noTarget {
t.Errorf("%s: to %q window %q consumers %d no target %v", c.name, to, window, n, r.NoTarget)
}
}
}
// fourOnA is four consumers bound to a, at 99 % of its weekly window, b with 70 % room and c with 60 %.
func fourOnA(t *testing.T) *miniMesh {
mm := newMesh(t)
ctx := context.Background()
for _, l := range []Licence{sub("a", read(40, 99)), sub("b", read(30, 30)), sub("c", read(40, 40))} {
_ = mm.store.SaveLicence(ctx, l)
}
for _, c := range []string{"ace", "g14", "laptop", "server"} {
_, _ = mm.store.Bind(ctx, c, "a", Move{Kind: "subscription", Why: "set", By: "test"})
}
mm.events = nil
return mm
}
// design 39 bus-and-switch §27/AC1, §26, §21: one act says one licence.switched with the consumers moved, the window,
// its percentage, reset and the target's room; one binding.changed per consumer; one audit entry switched with the same
// fields; each binding moved by usage, its why naming the licence, the window, the percentage and the reset.
func TestOneSwitchedPerAct(t *testing.T) {
mm := fourOnA(t)
acts, err := mm.m.ApplyRule(context.Background(), "", false, false)
if err != nil || len(acts) != 1 || acts[0].To != "b" || acts[0].Room != 70 {
t.Fatalf("acts %+v %v", acts, err)
}
var switched []map[string]any
changed := 0
for _, e := range mm.events {
name, body, _ := strings.Cut(e, " ")
var f map[string]any
_ = json.Unmarshal([]byte(body), &f)
switch name {
case "licence.switched":
switched = append(switched, f)
case "binding.changed":
changed++
}
}
if len(switched) != 1 || changed != 4 {
t.Fatalf("said %v", mm.events)
}
s := switched[0]
if s["from"] != "a" || s["to"] != "b" || len(s["consumers"].([]any)) != 4 || s["by"] != ByUsage ||
s["window"] != "weekly" || s["pct"] != 99.0 || s["resetsAt"] != "2026-11-14T09:00:00Z" || s["room"] != 70.0 {
t.Fatalf("licence.switched %v", s)
}
audit := mm.audit.under("switched")
if len(audit) != 1 || audit[0]["window"] != "weekly" || len(audit[0]["consumers"].([]any)) != 4 {
t.Fatalf("audit %v", audit)
}
b := mm.bound("laptop")
if b.Licence != "b" || b.By != ByUsage || b.Why != "a reached 99 % of its weekly window, which resets at 2026-11-14T09:00:00Z" {
t.Fatalf("laptop %+v", b)
}
}
// design 39 bus-and-switch §28/AC1: the rule tells the operator once per act, and the switch verb never.
func TestTheRuleTellsOnceAndTheVerbNever(t *testing.T) {
mm := fourOnA(t)
var told []string
mm.m.Tell = func(_ context.Context, headline string, lines []string) error {
told = append(told, headline+" | "+strings.Join(lines, " "))
return nil
}
_, _ = mm.m.ApplyRule(context.Background(), "", false, false)
if len(told) != 1 || told[0] != "ace, g14, laptop, server moved to b | a reached 99 % of its weekly window, "+
"which resets at 2026-11-14T09:00:00Z; b has 70 % room" {
t.Fatalf("told %v", told)
}
_, _ = mm.m.Bind(WithCause(context.Background(), Cause{Why: "the switch verb", By: "a person"}), "ace", "c")
if len(told) != 1 {
t.Fatalf("the switch verb told %v", told)
}
}
// design 39 bus-and-switch §32/AC1, §33/AC2: dry answers the same moves as the act and moves nothing; an invalid pair
// of settings, or switch_auto off, moves nothing after a reading, and the rule by hand runs with switch_auto off.
func TestDryAnswersTheSameMovesAndMovesNothing(t *testing.T) {
ctx := context.Background()
mm := fourOnA(t)
dry, err := mm.m.ApplyRule(ctx, "a", true, true)
if err != nil || len(dry) != 1 || mm.bound("ace").Licence != "a" || len(mm.events) != 0 {
t.Fatalf("dry %+v %v, said %v", dry, err, mm.events)
}
acts, _ := mm.m.ApplyRule(ctx, "a", true, false)
if len(acts) != 1 || acts[0].To != dry[0].To || strings.Join(acts[0].Consumers, ",") != strings.Join(dry[0].Consumers, ",") {
t.Fatalf("dry %+v, act %+v", dry, acts)
}
for name, change := range map[string]func(*Settings){
"an invalid pair": func(s *Settings) { s.RoomBelow = 100 },
"switch_auto off": func(s *Settings) { s.SwitchAuto = false },
} {
mm := fourOnA(t)
change(&mm.m.Settings)
if acts, _ := mm.m.ApplyRule(ctx, "", false, false); len(acts) != 0 || mm.bound("ace").Licence != "a" {
t.Errorf("%s moved %+v", name, acts)
}
}
mm = fourOnA(t)
mm.m.Settings.SwitchAuto = false
if acts, _ := mm.m.ApplyRule(ctx, "", true, false); len(acts) != 1 || mm.bound("ace").Licence != "b" {
t.Fatalf("by hand with switch_auto off moved %+v", acts)
}
}
// design 39 bus-and-switch §26: a reading at the threshold applies the rule at once.
func TestAReadingAtTheThresholdApplies(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: "b@example.org", Kind: "subscription", Usage: read(10, 10)})
mm.vendor.usage = map[string]any{"five_hour": map[string]any{"utilization": 30.0},
"seven_day": map[string]any{"utilization": 99.0, "resets_at": "2026-11-14T09:00:00Z"}}
if err := mm.m.ReadUsage(ctx); err != nil {
t.Fatal(err)
}
if b := mm.bound("laptop"); b.Licence != "b@example.org" || b.By != ByUsage {
t.Fatalf("laptop %+v", b)
}
}