Files
aml/pkg/models/models_test.go
T
zeekayandHanzo Dev 7c314d0b30 planes: the five surfaces a monitoring programme is operated through
Lists, suppressions, activations, the field catalog and the model plane. Each is
its own package, each is Base-backed, and none of them can destroy anything —
disposal is pkg/retention's decision alone, per tenant, and every one of them has
a test that reads its own source for a call to Delete.

lists holds the institution's own allow and deny lists, by class, normalised once
so an address written two ways is one address. It reaches a rule as one term,
Listed(name, value), and it refuses both a list nobody declared and an empty
value: either would be a rule that is catalogued, reported as coverage, and
incapable of firing. Removing a value stops it matching and keeps the row, because
a list entry is the reason a payment was refused.

suppress holds the decisions to keep a detection out of a queue. Reason and
decider are required and neither is defaulted; a suppression naming neither a rule
nor a subject is refused, because that is a kill switch. Lifting keeps the row.
The narrowest cover wins, so a specific decision about one account is the one named
on the activation rather than the rule-wide one behind it.

watch writes every activation before anything happens to it, and marks the ones a
suppression covered or a fold matched rather than dropping them: a system that
discards what it decided not to show cannot say how much it is not showing. Action
is what the rule asked for, Response is what the plane concluded, and exactly two
declared mechanisms move it. A rung may only raise — lowering is a suppression by
another name — and elevation beats folding, because an activation that reached an
escalation rung is the escalation. The live feed is a convenience over the durable
rows; it drops rather than blocking ingest, and the drops are counted per tenant
and published on every poll.

dictionary reports what a tenant's payloads carry: declared fields discovered by
reflection over the transaction rather than from a second list, the tenant's own
Raw keys, fill, blindness, and a distinct count kept as a bitmap sketch — hashed
with the tenant, so two tenants' bitmaps cannot be compared, and saturating into
"no longer a count" rather than drifting down. No value is ever stored. It is
write-behind and says so: Pending is exactly what a restart would lose.

topology searches the space of model shapes over a tenant's own history, through
the detector's own learning path rather than a copy of it, and reports the learning
curve, the realised alert share against the stated appetite, where each candidate
went blind, and which features carried the signal. It has no store and imports
nothing that has one. It names NO winner where nothing has been judged: ranking
needs an outcome, and a ranking invented from unlabelled data would look like a
recommendation and be a preference. Ties go to the smaller model.

models keeps every search and every fitted state, because both are governed acts,
and adopts a fit into the live model with its decider recorded. The learned state
never leaves the store — mass counters describe where a tenant's activity is dense.
A fit installs only into a model of the same shape, which is what stops a tenant's
model being swapped underneath it by restoring a file.

Around them: anomaly.Learn exposes the assessment Assess was already computing and
throwing away, so a study of the model reads the same scores production does;
engine gains one provider and one vocabulary term; and the tenant key moves to
pkg/brand, where the brand registry is, so the five new planes hold the same shape
at their own write boundary instead of each growing an opinion about what a tenant
is. api/tenant.go's qualify and qualified are now aliases of it, not a second copy.

Every operation is func(ctx, org, *In) (*Out, error): the tenant is an argument and
never something the operation resolves for itself. api/typed.go is the whole of
what turns one into a route — query into the input for a GET, body for a POST, the
path bound last and winning — and api/planes.go is a routing table with no handler
bodies in it, which a test reads as source. The contract is the Go types and
nothing else, so a zip face over the same functions restates none of it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 16:37:20 -07:00

378 lines
12 KiB
Go

package models
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
"github.com/luxfi/aml/internal/instance"
"github.com/luxfi/aml/pkg/anomaly"
"github.com/luxfi/aml/pkg/replay"
"github.com/luxfi/aml/pkg/topology"
"github.com/luxfi/aml/pkg/types"
"github.com/luxfi/aml/pkg/velocity"
)
const (
acme = "hanzo/acme"
rival = "hanzo/rival"
// other is the SAME org name under a different brand.
other = "zoo/acme"
)
var (
noon = time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
shape = topology.Topology{Trees: 8, Depth: 6, Window: 16, Blend: 0.25, Review: 0.02}
)
// history is a tenant's own traffic, with the outliers a human judged productive.
type history struct{ n int }
func (h history) History(ctx context.Context, org string) (replay.History, error) {
out := make(replay.Slice, 0, h.n)
for i := 0; i < h.n; i++ {
usd := 100 + float64(i%17)*3
odd := i%53 == 52
if odd {
usd = 90_000
}
e := replay.Event{
Tx: types.Transaction{
ID: "tx-" + time.Duration(i).String(), OrgID: org, UserID: "u1", AccountID: "acct-1",
Currency: "USD", Notional: usd, USD: usd, Direction: "in",
Timestamp: noon.Add(time.Duration(i) * time.Minute),
},
Entity: types.Entity{ID: "u1", OrgID: org},
}
switch {
case odd:
e.Disposition = replay.Productive
case i%23 == 0:
e.Disposition = replay.Unproductive
}
out = append(out, e)
}
return out, nil
}
func live(t *testing.T, s topology.Topology, seed uint64) *anomaly.Store {
t.Helper()
model, err := anomaly.New(s.Config(seed), velocity.New(velocity.Config{}))
if err != nil {
t.Fatal(err)
}
return model
}
func shelf(t *testing.T) *Shelf {
t.Helper()
app := instance.New(t)
t.Cleanup(app.Cleanup)
if err := Ensure(app); err != nil {
t.Fatalf("ensure: %v", err)
}
s := NewBase(app)
s.Now = func() time.Time { return noon }
s.History = history{n: 600}
s.Model = live(t, shape, 7)
return s
}
// TestASearchIsKept. An answer that reached a screen and not the store is one
// nobody can produce when asked why the model looks the way it does.
func TestASearchIsKept(t *testing.T) {
s := shelf(t)
ctx := context.Background()
run, err := s.Search(ctx, acme, &SearchIn{
Space: topology.Space{Trees: []int{8}, Depth: []int{6}, Window: []int{16}, Blend: []float64{0.25}, Review: []float64{0.02}},
Options: topology.Options{Seed: 7, Curve: 8},
By: "a.mensah",
})
if err != nil {
t.Fatal(err)
}
if len(run.Report.Trials) != 1 || run.Report.Seed != 7 {
t.Fatalf("the run must carry the whole report: %+v", run.Report)
}
back, err := s.Run(ctx, acme, &RefIn{ID: run.ID})
if err != nil {
t.Fatal(err)
}
if len(back.Report.Trials) != 1 || back.By != "a.mensah" || back.Report.Seed != 7 {
t.Fatalf("the run did not come back whole: %+v", back)
}
if len(back.Report.Trials[0].Curve) < 8 {
t.Fatalf("the learning curve did not survive the store: %d points", len(back.Report.Trials[0].Curve))
}
list, err := s.Runs(ctx, acme, &RunsIn{})
if err != nil {
t.Fatal(err)
}
if len(list.Runs) != 1 || list.Runs[0].ID != run.ID || list.Runs[0].Trials != 1 {
t.Fatalf("the list must summarise the run: %+v", list.Runs)
}
// A list is a summary and never the heavy read.
raw, err := json.Marshal(list)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "\"curve\"") {
t.Fatal("a list of runs must not carry a curve per candidate")
}
}
// TestASearchNeedsAHistoryAndADecider.
func TestASearchNeedsAHistoryAndADecider(t *testing.T) {
s := shelf(t)
ctx := context.Background()
if _, err := s.Search(ctx, acme, &SearchIn{Space: topology.Space{}}); !errors.Is(err, ErrDecider) {
t.Errorf("a search must name who asked for it, got %v", err)
}
s.History = nil
if _, err := s.Search(ctx, acme, &SearchIn{Space: topology.Space{}, By: "a"}); !errors.Is(err, ErrNoHistory) {
t.Errorf("a search over no history would report that every shape alerts on nothing, got %v", err)
}
}
// TestFitIsAdoptableOnlyIntoItsOwnShape. That is the governance property: a
// tenant's model shape cannot be swapped underneath it by restoring a file.
func TestFitIsAdoptableOnlyIntoItsOwnShape(t *testing.T) {
s := shelf(t)
ctx := context.Background()
mine, err := s.Fit(ctx, acme, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
if !mine.Adoptable {
t.Fatalf("a fit of the running shape must be adoptable: %+v", mine)
}
bigger := shape
bigger.Trees = 16
elsewhere, err := s.Fit(ctx, acme, &FitIn{Topology: bigger, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
if elsewhere.Adoptable {
t.Fatalf("a fit of a shape the deployment is not running must not be adoptable: %+v", elsewhere)
}
if _, err := s.Adopt(ctx, acme, &AdoptIn{ID: elsewhere.ID, Reason: "the search recommended it", By: "a.mensah"}); err == nil {
t.Fatal("adopting a fit of another shape must be refused by the model itself")
}
adopted, err := s.Adopt(ctx, acme, &AdoptIn{ID: mine.ID, Reason: "the search recommended it", By: "r.okafor"})
if err != nil {
t.Fatalf("adopt: %v", err)
}
if adopted.Adopted.IsZero() || adopted.AdoptedBy != "r.okafor" {
t.Fatalf("an adoption must name who took it and when: %+v", adopted)
}
if st := s.Model.State(acme); st.Learned == 0 {
t.Fatal("the live model did not take the fitted state")
}
if _, err := s.Adopt(ctx, acme, &AdoptIn{ID: mine.ID, Reason: "again", By: "r.okafor"}); !errors.Is(err, ErrAdopted) {
t.Fatalf("adopting twice must be refused, got %v", err)
}
}
// TestLearnedStateNeverLeavesTheStore.
//
// Mass counters describe where a tenant's activity is dense; handing them out
// over an API would publish the shape of an institution's customer behaviour to
// whoever holds a token.
func TestLearnedStateNeverLeavesTheStore(t *testing.T) {
s := shelf(t)
ctx := context.Background()
fit, err := s.Fit(ctx, acme, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(fit)
if err != nil {
t.Fatal(err)
}
var out map[string]any
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatal(err)
}
for _, leaked := range []string{"ref", "cur", "hist", "state", "snapshot", "seed"} {
if _, there := out[leaked]; there {
t.Errorf("a fit handed back %q — the learned state must stay in the tenant's store", leaked)
}
}
if fit.Digest == "" {
t.Fatal("the digest is what a caller compares, and must be on the answer")
}
}
// TestASnapshotCannotBeInstalledIntoAnotherTenant. The tenant comes from the row's
// own scope; a snapshot claiming otherwise is refused.
func TestASnapshotCannotBeInstalledIntoAnotherTenant(t *testing.T) {
s := shelf(t)
ctx := context.Background()
mine, err := s.Fit(ctx, acme, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
if _, err := s.Adopt(ctx, other, &AdoptIn{ID: mine.ID, Reason: "r", By: "b"}); !errors.Is(err, ErrNoFit) {
t.Fatalf("another tenant naming this fit's id must see no such fit, got %v", err)
}
if st := s.Model.State(other); st.Learned != 0 {
t.Fatal("another tenant's model took this tenant's state")
}
}
// TestTenantIsolation over every operation.
func TestTenantIsolation(t *testing.T) {
s := shelf(t)
ctx := context.Background()
run, err := s.Search(ctx, acme, &SearchIn{
Space: topology.Space{Trees: []int{8}, Depth: []int{6}, Window: []int{16}},
Options: topology.Options{Seed: 7, Curve: 4}, By: "a.mensah",
})
if err != nil {
t.Fatal(err)
}
fit, err := s.Fit(ctx, acme, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
for _, stranger := range []string{other, rival} {
if _, err := s.Run(ctx, stranger, &RefIn{ID: run.ID}); !errors.Is(err, ErrNotHere) {
t.Errorf("%s can read %s's run: %v", stranger, acme, err)
}
list, err := s.Runs(ctx, stranger, &RunsIn{})
if err != nil {
t.Fatal(err)
}
if len(list.Runs) != 0 {
t.Errorf("%s can list %s's runs", stranger, acme)
}
fits, err := s.Fits(ctx, stranger, &FitsIn{})
if err != nil {
t.Fatal(err)
}
if len(fits.Fits) != 0 {
t.Errorf("%s can list %s's fits", stranger, acme)
}
if _, err := s.Adopt(ctx, stranger, &AdoptIn{ID: fit.ID, Reason: "r", By: "b"}); !errors.Is(err, ErrNoFit) {
t.Errorf("%s can adopt %s's fit: %v", stranger, acme, err)
}
}
}
// TestBareOrgIsRefused.
func TestBareOrgIsRefused(t *testing.T) {
s := shelf(t)
ctx := context.Background()
for _, bare := range []string{"acme", "", "unknown/acme"} {
if _, err := s.Search(ctx, bare, &SearchIn{By: "a"}); err == nil {
t.Errorf("Search accepted %q as a tenant", bare)
}
if _, err := s.Fit(ctx, bare, &FitIn{Topology: shape, By: "a"}); err == nil {
t.Errorf("Fit accepted %q as a tenant", bare)
}
if _, err := s.Runs(ctx, bare, &RunsIn{}); err == nil {
t.Errorf("Runs accepted %q as a tenant", bare)
}
}
}
// TestNothingDeletes.
func TestNothingDeletes(t *testing.T) {
for _, name := range []string{"models.go", "shelf.go"} {
raw, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), ".Delete(") {
t.Errorf("%s calls Delete: disposal is pkg/retention's decision and nobody else's", name)
}
}
}
// TestRestart: the study that justified a model, and the state it produced, are
// both still there after the pod that made them is gone.
func TestRestart(t *testing.T) {
first := instance.New(t)
if err := Ensure(first); err != nil {
t.Fatal(err)
}
before := NewBase(first)
before.Now = func() time.Time { return noon }
before.History = history{n: 600}
before.Model = live(t, shape, 7)
ctx := context.Background()
run, err := before.Search(ctx, acme, &SearchIn{
Space: topology.Space{Trees: []int{8}, Depth: []int{6}, Window: []int{16}, Review: []float64{0.02}},
Options: topology.Options{Seed: 7, Curve: 8}, By: "a.mensah",
})
if err != nil {
t.Fatal(err)
}
fit, err := before.Fit(ctx, acme, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "a.mensah"})
if err != nil {
t.Fatal(err)
}
if _, err := before.Fit(ctx, other, &FitIn{Topology: shape, Options: topology.Options{Seed: 7}, By: "z.other"}); err != nil {
t.Fatal(err)
}
second := instance.Restart(t, first)
if err := Ensure(second); err != nil {
t.Fatal(err)
}
after := NewBase(second)
after.Now = func() time.Time { return noon }
after.History = history{n: 600}
// A FRESH model: warming, nothing learned. This is what a rollout produces.
after.Model = live(t, shape, 7)
back, err := after.Run(ctx, acme, &RefIn{ID: run.ID})
if err != nil {
t.Fatalf("the study did not survive the restart: %v", err)
}
if back.By != "a.mensah" || len(back.Report.Trials) == 0 {
t.Fatalf("the study came back changed: %+v", back)
}
if st := after.Model.State(acme); st.Learned != 0 {
t.Fatal("the fresh model should be warming — the test is not testing a restart")
}
adopted, err := after.Adopt(ctx, acme, &AdoptIn{ID: fit.ID, Reason: "restoring after a rollout", By: "r.okafor"})
if err != nil {
t.Fatalf("the fitted state did not survive the restart: %v", err)
}
if adopted.Digest != fit.Digest {
t.Fatalf("the fit came back under a different shape: %s vs %s", adopted.Digest, fit.Digest)
}
if st := after.Model.State(acme); st.Learned == 0 {
t.Fatal("a restart plus an adoption must leave the model warm, or every rollout is a blind window")
}
// Tenancy still holds.
fits, err := after.Fits(ctx, rival, &FitsIn{})
if err != nil {
t.Fatal(err)
}
if len(fits.Fits) != 0 {
t.Fatal("a third tenant can list fits after the restart")
}
mine, err := after.Fits(ctx, acme, &FitsIn{})
if err != nil {
t.Fatal(err)
}
if len(mine.Fits) != 1 {
t.Fatalf("the tenant boundary did not survive the restart: %d fits", len(mine.Fits))
}
}