package aioe import ( "reflect" "testing" "github.com/shopspring/decimal" ) func dec(s string) decimal.Decimal { return decimal.RequireFromString(s) } func sumOf(m map[string]decimal.Decimal) decimal.Decimal { s := decimal.Zero for _, d := range Dimensions { s = s.Add(m[d]) } return s } // eventsFor builds n identical events: positive outcomes where skills scored high and location // scored low, the canonical "skills matter more here" evidence shape. func eventsFor(n int) []Event { out := make([]Event, 0, n) for i := 0; i < n; i++ { out = append(out, Event{ Signal: dec("1"), DimScores: map[string]decimal.Decimal{ "clearance": dec("100"), "skills": dec("95"), "certifications": dec("50"), "customer_experience": dec("50"), "experience": dec("60"), "location": dec("10"), }, }) } return out } func TestComputeUpdateDeterministic(t *testing.T) { p := DefaultParams() a1, c1, u1 := ComputeUpdate(DefaultWeightsMap(), eventsFor(40), p) a2, c2, u2 := ComputeUpdate(DefaultWeightsMap(), eventsFor(40), p) if !reflect.DeepEqual(a1, a2) || !reflect.DeepEqual(c1, c2) || u1 != u2 { t.Fatalf("same inputs produced different outputs:\n%v\n%v", a1, a2) } } func TestComputeUpdateBelowMinEventsIsANoOp(t *testing.T) { p := DefaultParams() old := DefaultWeightsMap() next, changes, used := ComputeUpdate(old, eventsFor(p.MinEvents-1), p) if len(changes) != 0 { t.Fatalf("below MinEvents must not change weights, got %d changes", len(changes)) } if used != p.MinEvents-1 { t.Fatalf("used = %d", used) } if !reflect.DeepEqual(next, old) { t.Fatal("weights drifted on a no op") } } func TestComputeUpdateMovesTowardEvidenceWithinBounds(t *testing.T) { p := DefaultParams() old := DefaultWeightsMap() next, changes, _ := ComputeUpdate(old, eventsFor(100), p) if len(changes) == 0 { t.Fatal("strong uniform evidence over 100 outcomes should move weights") } if !next["skills"].GreaterThan(old["skills"]) { t.Fatalf("skills should rise on positive high skill evidence: %s -> %s", old["skills"], next["skills"]) } if !next["location"].LessThan(old["location"]) { t.Fatalf("location should fall on positive low location evidence: %s -> %s", old["location"], next["location"]) } if !sumOf(next).Equal(decimal.NewFromInt(1)) { t.Fatalf("weights must sum to exactly 1, got %s", sumOf(next)) } for _, d := range Dimensions { if next[d].LessThan(p.Floor) || next[d].GreaterThan(p.Ceiling) { t.Fatalf("dimension %s out of clamps: %s", d, next[d]) } } } // TestComputeUpdateStepBound proves no dimension's evidence driven move exceeds MaxStep by more // than the sum restoring redistribution can add, and repeated cycles stay clamped at the // floor and ceiling rather than running away. func TestComputeUpdateConvergesUnderClamps(t *testing.T) { p := DefaultParams() w := DefaultWeightsMap() for cycle := 0; cycle < 60; cycle++ { next, _, _ := ComputeUpdate(w, eventsFor(200), p) if !sumOf(next).Equal(decimal.NewFromInt(1)) { t.Fatalf("cycle %d: sum drifted to %s", cycle, sumOf(next)) } for _, d := range Dimensions { if next[d].LessThan(p.Floor) || next[d].GreaterThan(p.Ceiling) { t.Fatalf("cycle %d: %s escaped clamps: %s", cycle, d, next[d]) } } w = next } // Sixty cycles of maximal one directional evidence must have pushed skills to (or near) the // ceiling and location to (or near) the floor, proving clamps hold at the extremes. if w["skills"].LessThan(dec("0.30")) { t.Fatalf("skills should approach the ceiling after sustained evidence, got %s", w["skills"]) } if w["location"].GreaterThan(dec("0.10")) { t.Fatalf("location should approach the floor after sustained evidence, got %s", w["location"]) } } func TestComputeUpdateMixedSignalsCancel(t *testing.T) { p := DefaultParams() scores := map[string]decimal.Decimal{ "clearance": dec("80"), "skills": dec("80"), "certifications": dec("80"), "customer_experience": dec("80"), "experience": dec("80"), "location": dec("80"), } var events []Event for i := 0; i < 30; i++ { events = append(events, Event{Signal: dec("1"), DimScores: scores}) events = append(events, Event{Signal: dec("-1"), DimScores: scores}) } next, changes, _ := ComputeUpdate(DefaultWeightsMap(), events, p) if len(changes) != 0 { t.Fatalf("perfectly cancelling evidence must not move weights, got %v", changes) } if !reflect.DeepEqual(next, DefaultWeightsMap()) { t.Fatal("weights changed on cancelling evidence") } } func TestComputeUpdateEventsWithoutScoresCountTowardWindowOnly(t *testing.T) { p := DefaultParams() var events []Event for i := 0; i < 40; i++ { events = append(events, Event{Signal: dec("1")}) // no dimension scores captured } next, changes, used := ComputeUpdate(DefaultWeightsMap(), events, p) if used != 40 { t.Fatalf("used = %d", used) } if len(changes) != 0 { t.Fatalf("score free events carry no evidence, weights must hold, got %v", changes) } if !sumOf(next).Equal(decimal.NewFromInt(1)) { t.Fatalf("sum broke: %s", sumOf(next)) } }