package main import ( "context" "encoding/json" "errors" "os" "path/filepath" "regexp" "slices" "sort" "strings" "testing" "time" oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" "claude-licence-manager/internal/licences" ) // runtime is a fixture of the runtime's bus-state and log calls (design 39 parts §3): it answers every read from what // a test seeds, and fails the test on any write or append the tools bundle makes. type runtime struct { t *testing.T states map[string]map[string]json.RawMessage logs map[string][]stdio.LogEntry } func newRuntime(t *testing.T) *runtime { rt := &runtime{t: t, states: map[string]map[string]json.RawMessage{}, logs: map[string][]stdio.LogEntry{}} for _, s := range licences.States { rt.states[s] = map[string]json.RawMessage{} } for _, l := range licences.Logs { rt.logs[l] = nil } return rt } // seedState and seedLog keep what the manager wrote before the tools were asked. func (rt *runtime) seedState(state, key string, v any) { raw, _ := json.Marshal(v) rt.states[state][key] = raw } func (rt *runtime) seedLog(log, key string, v any) { raw, _ := json.Marshal(v) rt.logs[log] = append(rt.logs[log], stdio.LogEntry{Sequence: uint64(len(rt.logs[log]) + 1), Key: key, Value: raw}) } func (rt *runtime) reader() *Reader { r := &Reader{States: map[string]licences.State{}, Logs: map[string]licences.Log{}} for s := range rt.states { r.States[s] = keptState{rt, s} } for l := range rt.logs { r.Logs[l] = keptLog{rt, l} } return r } type keptState struct { rt *runtime name string } func (s keptState) wrote(how, key string) error { s.rt.t.Errorf("the tools bundle %s %s.%s: it writes nothing", how, s.name, key) return errors.New("refused: the tools bundle writes nothing") } func (s keptState) Get(key string) (*stdio.StateEntry, error) { v, ok := s.rt.states[s.name][key] if !ok { return nil, nil } return &stdio.StateEntry{Key: key, Value: v, Revision: 1}, nil } func (s keptState) Put(key string, _ any) (uint64, error) { return 0, s.wrote("put", key) } func (s keptState) PutTTL(key string, _ any, _ time.Duration) (uint64, error) { return 0, s.wrote("put", key) } func (s keptState) Create(key string, _ any) (uint64, error) { return 0, s.wrote("created", key) } func (s keptState) CreateTTL(key string, _ any, _ time.Duration) (uint64, error) { return 0, s.wrote("created", key) } func (s keptState) Delete(key string) error { return s.wrote("deleted", key) } func (s keptState) Keys() ([]string, error) { keys := []string{} for k := range s.rt.states[s.name] { keys = append(keys, k) } sort.Strings(keys) return keys, nil } func (s keptState) History(key string) ([]stdio.StateRevision, error) { return nil, nil } type keptLog struct { rt *runtime name string } func (l keptLog) Append(key string, _ any) (uint64, error) { l.rt.t.Errorf("the tools bundle appended to %s.%s: it writes nothing", l.name, key) return 0, errors.New("refused: the tools bundle writes nothing") } func (l keptLog) Read(from uint64, keys string, _ int) (*stdio.LogPage, error) { page := &stdio.LogPage{Last: uint64(len(l.rt.logs[l.name]))} for _, e := range l.rt.logs[l.name] { if e.Sequence >= from && (keys == "" || e.Key == keys) { page.Entries = append(page.Entries, e) } } return page, nil } func (l keptLog) Last(key string) (*stdio.LogEntry, error) { var last *stdio.LogEntry for _, e := range l.rt.logs[l.name] { if e.Key == key { last = &e } } return last, nil } func (l keptLog) Info() (*stdio.LogInfo, error) { n := uint64(len(l.rt.logs[l.name])) return &stdio.LogInfo{Entries: n, Last: n, First: min(n, 1), MaxBytes: 64 << 20}, nil } func (l keptLog) Newest(string) ([]stdio.LogEntry, error) { by := map[string]stdio.LogEntry{} for _, e := range l.rt.logs[l.name] { by[e.Key] = e } out := []stdio.LogEntry{} for _, e := range by { out = append(out, e) } return out, nil } // run is one tool's answer as JSON, decoded into a generic form. func run(t *testing.T, r *Reader, name string, args map[string]any) any { t.Helper() for _, tool := range r.Tools() { if tool.Name != name { continue } got, err := tool.Run(args) if err != nil { t.Fatalf("%s %v answered %v", name, args, err) } raw, _ := json.Marshal(got) var v any _ = json.Unmarshal(raw, &v) return v } t.Fatalf("the tools bundle registers no %s", name) return nil } // design 39 parts §3, §4: the tools bundle registers exactly the four reading tools; its code emits nothing, // subscribes to no event, asks no heartbeat, calls neither the vendor nor the vault, and asks nothing but mesh-config. func TestTheToolsBundleRegistersTheFourReadingToolsAndActsOnNothing(t *testing.T) { var names []string for _, tool := range newRuntime(t).reader().Tools() { names = append(names, tool.Name) } sort.Strings(names) if want := []string{"licences_conditions", "licences_kept", "licences_settings", "licences_switches"}; !slices.Equal(names, want) { t.Fatalf("the tools bundle registers %v, not %v", names, want) } files, _ := filepath.Glob("*.go") acts := regexp.MustCompile(`stdio\.(Emit|Subscribe|SeatPublish|Publish|Heartbeat|Watch)\b|\.(Put|PutTTL|Create|CreateTTL|Delete|Append)\(|os\.(WriteFile|Create|OpenFile)|http\.`) asked := regexp.MustCompile(`stdio\.Ask\("([^"]+)"`) for _, f := range files { if strings.HasSuffix(f, "_test.go") { continue } src, err := os.ReadFile(f) if err != nil { t.Fatal(err) } for _, m := range acts.FindAllString(string(src), -1) { t.Errorf("%s calls %s: the tools bundle acts on nothing", f, m) } for _, m := range asked.FindAllStringSubmatch(string(src), -1) { if m[1] != "seat:mesh-config.config" { t.Errorf("%s asks %s: the tools bundle asks only mesh-config", f, m[1]) } } } } // design 39 bus-and-switch §37/AC1, design 39 parts §3: a switch by usage and one by a person are answered, newest // first, with their causes, a switch by usage with the reading that moved them; licence and consumer narrow the // answer; nothing is written. func TestSwitchesAnswersBothCauses(t *testing.T) { rt := newRuntime(t) rt.seedLog(licences.EntryAudit, "switched", map[string]any{"at": "2026-11-12T10:00:00Z", "from": "a", "to": "b", "consumers": []string{"ace", "g14"}, "by": "usage", "why": "a at 99 %", "window": "weekly", "pct": 99, "resetsAt": "2026-11-14T09:00:00Z", "room": 60}) rt.seedLog(licences.EntryAudit, "bound", map[string]any{"at": "2026-11-12T10:00:30Z", "consumer": "ace"}) rt.seedLog(licences.EntryAudit, "switched", map[string]any{"at": "2026-11-12T10:01:00Z", "from": "b", "to": "c", "consumers": []string{"ace"}, "by": "a person", "why": "the switch verb"}) r := rt.reader() got := run(t, r, "licences_switches", nil).([]any) if len(got) != 2 { t.Fatalf("switches %v", got) } person, usage := got[0].(map[string]any), got[1].(map[string]any) if person["by"] != "a person" || person["why"] != "the switch verb" || person["to"] != "c" || person["reading"] != nil || usage["by"] != "usage" || usage["reading"].(map[string]any)["window"] != "weekly" || usage["from"] != "a" { t.Fatalf("switches %v", got) } if got := run(t, r, "licences_switches", map[string]any{"licence": "c"}).([]any); len(got) != 1 { t.Fatalf("of c %v", got) } if got := run(t, r, "licences_switches", map[string]any{"consumer": "g14"}).([]any); len(got) != 1 || got[0].(map[string]any)["by"] != "usage" { t.Fatalf("of g14 %v", got) } if got := run(t, r, "licences_switches", map[string]any{"since": "2026-11-12T10:00:30Z"}).([]any); len(got) != 1 { t.Fatalf("since %v", got) } } // design 39 bus-and-switch §37, §12: a switch carried over from the old table keeps the old field names (consumer, // licence) and is answered with `to` and `consumers`, and found by them. func TestSwitchesAnswersCarriedEntries(t *testing.T) { rt := newRuntime(t) rt.seedLog(licences.EntryAudit, "switched", map[string]any{"at": "2026-11-11T21:10:33Z", "consumer": "n1", "licence": "d@example.org", "from": "c@example.org", "by": "a@example.org"}) r := rt.reader() for _, args := range []map[string]any{nil, {"consumer": "n1"}, {"licence": "d@example.org"}} { got := run(t, r, "licences_switches", args).([]any) if len(got) != 1 { t.Fatalf("%v: %v", args, got) } e := got[0].(map[string]any) if e["to"] != "d@example.org" || e["from"] != "c@example.org" || e["by"] != "a@example.org" || len(e["consumers"].([]any)) != 1 || e["consumers"].([]any)[0] != "n1" { t.Fatalf("%v: %v", args, e) } } } // design 39 bus-and-switch §40/AC1, B18, design 39 parts §3: a value set at node level is answered with its level, the // rest with the default, each with its value and default, and an invalid one saying why, read from the settings file; // mesh-config not answering is an error, never the default. func TestSettingsAnswersTheLevel(t *testing.T) { r := newRuntime(t).reader() r.SettingsFile = filepath.Join(t.TempDir(), "settings.json") if err := os.WriteFile(r.SettingsFile, []byte(`{"switch_at_pct": 80, "warn_at_pct": 85, "usage_minutes": 900}`), 0o600); err != nil { t.Fatal(err) } r.ConfigLevels = func(context.Context) (map[string]string, error) { return map[string]string{"switch_at_pct": "node", "warn_at_pct": "mesh", "cadence_minutes": "default"}, nil } by := map[string]map[string]any{} for _, s := range run(t, r, "licences_settings", nil).([]any) { f := s.(map[string]any) by[f["setting"].(string)] = f } str := func(v any) string { s, _ := v.(string); return s } if len(by) != len(licences.Defaults.Values()) || by["switch_at_pct"]["level"] != "node" || by["switch_at_pct"]["value"] != 80.0 || by["switch_at_pct"]["default"] != 99.0 || by["usage_minutes"]["level"] != "default" || by["usage_minutes"]["value"] != 5.0 || !strings.Contains(str(by["usage_minutes"]["invalid"]), "outside its bound") || !strings.Contains(str(by["room_below_pct"]["invalid"]), "above switch_at_pct 80") || !strings.Contains(str(by["warn_at_pct"]["invalid"]), "85") || by["switch_auto"]["invalid"] != nil { t.Fatalf("settings %v", by) } r.ConfigLevels = func(context.Context) (map[string]string, error) { return nil, errors.New("no responders") } for _, tool := range r.Tools() { if tool.Name == "licences_settings" { if _, err := tool.Run(nil); err == nil { t.Fatal("mesh-config not answering is answered as the default") } } } } // design 39 bus-and-switch §41/AC1, design 39 parts §3, §10: every bus-state entry the manifest asks for is read by // licences_kept, with its keys or subjects, one key's value and its fill, the carry-over's record among them; an // entry not kept is refused; nothing is written. func TestKeptReadsEveryEntryOfTheManifest(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { Requires []struct { Name string `json:"name"` Template string `json:"template"` } `json:"requires"` Logs []struct { Name string `json:"name"` } `json:"logs"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } rt := newRuntime(t) rt.seedState(licences.EntryLicences, "a_at_example.org", licences.Licence{Name: "a@example.org", Kind: "subscription"}) rt.seedState(licences.EntryCarried, "postgres", map[string]any{"at": "2026-11-12T09:44:59Z"}) rt.seedLog(licences.EntryUsage, "a_at_example.org", licences.UsageEntry{At: "2026-11-12T10:00:00Z"}) r := rt.reader() all := run(t, r, "licences_kept", nil).(map[string]any) kinds := map[string]string{} for _, e := range m.Requires { kinds[e.Name] = e.Template } for _, l := range m.Logs { kinds[l.Name] = "log" } for name, kind := range kinds { got, ok := all[name].(map[string]any) if !ok || got["fill"] == nil || got["kind"] != kind { t.Errorf("licences_kept does not read %s as a %s: %v", name, kind, all[name]) } } if len(kinds) != 8 || len(all) != len(kinds) { t.Fatalf("the manifest asks for %d entries, licences_kept reads %d", len(kinds), len(all)) } one := run(t, r, "licences_kept", map[string]any{"entry": "carried", "key": "postgres"}).(map[string]any) if v, _ := one["carried"].(map[string]any)["value"].(map[string]any); v["at"] != "2026-11-12T09:44:59Z" { t.Fatalf("the carry-over's record %v", one) } if subjects := all["usage"].(map[string]any)["subjects"].(map[string]any); subjects["a_at_example.org"] == nil { t.Fatalf("usage subjects %v", subjects) } if _, err := r.Kept("tables", ""); err == nil { t.Fatal("an entry not kept is answered") } } // design 39 bus-and-switch §43/AC1, design 39 parts §3: each key of conditions is answered with its body and since. func TestConditionsAnswersEachKey(t *testing.T) { rt := newRuntime(t) for _, k := range []string{"settings.room_below_pct.invalid", "licence.a.near-limit"} { rt.seedState(licences.EntryConditions, k, licences.KeptCondition{Body: oc.Condition{Key: k, Headline: "h"}, Since: "2026-11-12T10:00:00Z"}) } var keys []string for _, c := range run(t, rt.reader(), "licences_conditions", nil).([]any) { f := c.(map[string]any) if f["since"] == "" || f["body"].(map[string]any)["headline"] == "" { t.Fatalf("condition %v", f) } keys = append(keys, f["body"].(map[string]any)["key"].(string)) } if strings.Join(keys, ",") != "licence.a.near-limit,settings.room_below_pct.invalid" { t.Fatalf("keys %v", keys) } }