package main // The keeper's bus shapes carry only their named fields (novox/hq ADR 0507 ยง1, AC7; ADR 0508): every key's value // and every event's body, filled whole, marshals to exactly the fields named here, and no shape holds a map or a // free-form value through which anything else could reach the bus. import ( "encoding/json" "reflect" "regexp" "sort" "strings" "testing" "time" ) // busFields is every shape's JSON fields, by Go type: the interface the keeper and the verbs share. var busFields = map[string][]string{ "InstalledValue": {"at", "packages"}, "InstalledPackage": {"name", "version", "explicit", "repo"}, "NotCurrent": {"bytes", "since"}, "UpdatesValue": {"reachable", "packages"}, "VersionChange": {"name", "from", "to"}, "PackageVersion": {"name", "version"}, "Run": {"node", "run", "kind", "from", "round", "ask", "of", "steps"}, "Step": {"step", "at", "why", "held", "unit", "ignored", "plan", "exit", "error", "changes", "check"}, "HeldKernel": {"package", "version", "module", "bound"}, "Changes": {"added", "removed", "changed"}, "Check": {"passed", "pacnew", "pacsave", "news", "news_error", "mapped", "mapped_error", "newly_failed", "restart_needed", "running_kernel", "installed_kernel", "unbuilt", "dkms_error"}, "NewsTitle": {"title", "link", "published"}, "Mapped": {"unit", "container", "executable"}, "DKMSBuild": {"module", "version", "kernel"}, "RollbackItem": {"name", "from", "to", "source", "location"}, "Record": {"node", "run", "at", "installed", "kernel", "failed_units", "dkms"}, "DKMSModule": {"module", "version", "kernels"}, "Round": {"round", "opened", "opened_by", "order"}, "RoundNode": {"round", "node", "run", "pass"}, "Pass": {"reason", "ask", "asked", "answer", "answered"}, "Ask": {"id", "node", "act", "at", "of", "remove_added", "round", "pass", "reason"}, "PackageChanged": {"node", "at", "added", "removed", "changed"}, "UpgradeRecorded": {"node", "run", "packages", "kernel"}, "KernelHeld": {"node", "run", "held"}, "UpgradeStarted": {"node", "run", "from", "round", "unit", "ignored"}, "UpgradeFinished": {"node", "run", "changes"}, "UpgradeFailed": {"node", "run", "step", "exit", "error"}, "UpgradeChecked": {"node", "run", "kind", "passed"}, "RollbackStarted": {"node", "run", "of", "unit"}, "RollbackFinished": {"node", "run", "of", "exit", "error"}, } // The value of each state's keys. var stateValues = []any{InstalledValue{}, NotCurrent{}, UpdatesValue{}, Run{}, Record{}, Round{}, RoundNode{}, Ask{}} var stamp = time.Date(2026, 10, 24, 4, 0, 12, 0, time.UTC) // fill sets every field of v, recursively, to a value that is not empty: one element in each list, every pointer // set, so that every field marshals, omitempty or not. func fill(v reflect.Value) { switch v.Kind() { case reflect.String: v.SetString("x") case reflect.Bool: v.SetBool(true) case reflect.Int, reflect.Int64: v.SetInt(1) case reflect.Pointer: v.Set(reflect.New(v.Type().Elem())) fill(v.Elem()) case reflect.Slice: s := reflect.MakeSlice(v.Type(), 1, 1) fill(s.Index(0)) v.Set(s) case reflect.Struct: if v.Type() == reflect.TypeOf(time.Time{}) { v.Set(reflect.ValueOf(stamp)) return } for i := 0; i < v.NumField(); i++ { fill(v.Field(i)) } } } // fieldsOf walks a filled value against its marshalled JSON: each struct's object holds exactly its named // fields. It answers the struct types it met. func fieldsOf(t *testing.T, v reflect.Value, raw json.RawMessage, met map[string]bool) { t.Helper() switch v.Kind() { case reflect.Map, reflect.Interface: t.Errorf("%s holds a free-form value: a bus shape names its fields", v.Type()) case reflect.Pointer: fieldsOf(t, v.Elem(), raw, met) case reflect.Slice: var items []json.RawMessage if err := json.Unmarshal(raw, &items); err != nil || len(items) != v.Len() { t.Errorf("%s: %s is not its list", v.Type(), raw) return } for i := range items { fieldsOf(t, v.Index(i), items[i], met) } case reflect.Struct: if v.Type() == reflect.TypeOf(time.Time{}) { return } name := v.Type().Name() met[name] = true var obj map[string]json.RawMessage if err := json.Unmarshal(raw, &obj); err != nil { t.Errorf("%s: %s is not an object", name, raw) return } var got []string for k := range obj { got = append(got, k) } want, named := busFields[name] if !named { t.Errorf("%s is on the bus and its fields are not named in busFields", name) return } want = append([]string(nil), want...) sort.Strings(got) sort.Strings(want) if strings.Join(got, " ") != strings.Join(want, " ") { t.Errorf("%s marshals to %v, its named fields are %v", name, got, want) } for i := 0; i < v.NumField(); i++ { tag, _, _ := strings.Cut(v.Type().Field(i).Tag.Get("json"), ",") if r, ok := obj[tag]; ok { fieldsOf(t, v.Field(i), r, met) } } } } func TestEveryStateValueAndEventBodyCarriesOnlyItsNamedFields(t *testing.T) { roots := append([]any(nil), stateValues...) for _, e := range Events { roots = append(roots, e.Body) } met := map[string]bool{} for _, root := range roots { v := reflect.New(reflect.TypeOf(root)).Elem() fill(v) raw, err := json.Marshal(v.Interface()) if err != nil { t.Fatal(err) } fieldsOf(t, v, raw, met) } for name := range busFields { if !met[name] { t.Errorf("busFields names %s, which no state value or event body holds", name) } } } func TestEveryEventIsNamedOnceWithItsBody(t *testing.T) { seen := map[string]bool{} for _, e := range Events { if seen[e.Name] { t.Errorf("%s twice", e.Name) } seen[e.Name] = true if e.Body == nil || reflect.TypeOf(e.Body).Kind() != reflect.Struct { t.Errorf("%s has no body type", e.Name) } } if len(Events) != 9 { t.Errorf("package.changed and the eight upgrade and rollback events, not %d", len(Events)) } } // A key is node and id tokens joined by dots; each token is one NATS key token. func TestKeysAreTokensJoinedByDots(t *testing.T) { token := regexp.MustCompile(`^[A-Za-z0-9_-]+$`) run := RunID(time.Date(2026, 10, 24, 6, 0, 12, 0, time.FixedZone("CEST", 2*3600))) if run != "20261024T040012Z" { t.Errorf("RunID: %s", run) } round := RoundID(time.Date(2026, 10, 24, 4, 0, 0, 0, time.Local)) if round != "2026-W43" { t.Errorf("RoundID: %s", round) } if RoundID(time.Date(2027, 1, 2, 4, 0, 0, 0, time.UTC)) != "2026-W53" { t.Errorf("the ISO week of 2 January 2027 is 2026's 53rd: %s", RoundID(time.Date(2027, 1, 2, 4, 0, 0, 0, time.UTC))) } for key, parts := range map[string]int{ NodeKey("node-a", run): 2, NotCurrentKey("node-a"): 2, RoundNodeKey(round, "node-b"): 2, } { ts := strings.Split(key, ".") if len(ts) != parts { t.Errorf("%s: %d tokens", key, len(ts)) } for _, x := range ts { if !token.MatchString(x) { t.Errorf("%s: token %q", key, x) } } } }