mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
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
341 lines
12 KiB
Go
341 lines
12 KiB
Go
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 §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)
|
|
}
|
|
}
|