package emotion import ( "errors" "fmt" "sync" "testing" ) func mustMachine(t *testing.T, s EmotionState) *Machine { t.Helper() m, err := NewMachine(Params{InitialState: s}) if err != nil { t.Fatalf("NewMachine(%q) error: %v", s, err) } return m } func TestManager_EmotionFollowsPersona(t *testing.T) { t.Parallel() mg := NewManager() a, err := NewAffection(50, 0, 100) if err != nil { t.Fatalf("NewAffection() error: %v", err) } mg.Set("personaA", mustMachine(t, Excited), a) mg.Set("personaB", mustMachine(t, Calm), nil) // Transition personaA; personaB must be untouched. if err := mg.Transition("personaA", Happy); err != nil { t.Fatalf("Transition(personaA, happy) error: %v", err) } mA, _, ok := mg.Get("personaA") if !ok { t.Fatal("personaA not found") } if mA.Current() != Happy { t.Fatalf("personaA Current() = %q, want %q", mA.Current(), Happy) } mB, _, _ := mg.Get("personaB") if mB.Current() != Calm { t.Fatalf("personaB Current() = %q, want %q (isolation)", mB.Current(), Calm) } } func TestManager_AdjustAffectionIsolated(t *testing.T) { t.Parallel() mg := NewManager() affA, err := NewAffection(40, 0, 100) if err != nil { t.Fatalf("NewAffection() error: %v", err) } affB, err := NewAffection(10, 0, 100) if err != nil { t.Fatalf("NewAffection() error: %v", err) } mg.Set("personaA", mustMachine(t, Calm), affA) mg.Set("personaB", mustMachine(t, Calm), affB) before, after, err := mg.AdjustAffection("personaA", 30) if err != nil { t.Fatalf("AdjustAffection(personaA) error: %v", err) } if before != 40 || after != 70 { t.Fatalf("AdjustAffection(personaA) before/after = %d/%d, want 40/70", before, after) } if got := affB.Value(); got != 10 { t.Fatalf("personaB value = %d, want 10 (isolation)", got) } } func TestManager_TransitionUnknownPersona(t *testing.T) { t.Parallel() mg := NewManager() mg.Set("personaA", mustMachine(t, Calm), nil) err := mg.Transition("ghost", Happy) if !errors.Is(err, ErrUnknownPersona) { t.Fatalf("Transition(ghost) error = %v, want ErrUnknownPersona", err) } if _, _, err := mg.AdjustAffection("ghost", 1); !errors.Is(err, ErrUnknownPersona) { t.Fatalf("AdjustAffection(ghost) error = %v, want ErrUnknownPersona", err) } } func TestManager_SetGetRemoveCount(t *testing.T) { t.Parallel() mg := NewManager() mg.Set("a", mustMachine(t, Calm), nil) mg.Set("b", mustMachine(t, Happy), nil) if got := mg.Count(); got != 2 { t.Fatalf("Count() = %d, want 2", got) } if _, _, ok := mg.Get("a"); !ok { t.Fatal("Get(a) not found") } if !mg.Remove("a") { t.Fatal("Remove(a) should report true") } if mg.Remove("a") { t.Fatal("Remove(a) again should report false") } if got := mg.Count(); got != 1 { t.Fatalf("Count() after remove = %d, want 1", got) } } func TestManager_ConcurrentAccess(t *testing.T) { t.Parallel() mg := NewManager() const n = 8 for i := 0; i < n; i++ { id := idOf(i) aff, _ := NewAffection(50, 0, 100) mg.Set(id, mustMachine(t, Excited), aff) } var wg sync.WaitGroup for i := 0; i < n; i++ { id := idOf(i) wg.Add(1) go func() { defer wg.Done() _, _, _ = mg.Get(id) _, _, _ = mg.AdjustAffection(id, 5) _ = mg.Transition(id, Happy) }() } wg.Wait() for i := 0; i < n; i++ { id := idOf(i) m, _, ok := mg.Get(id) if !ok { t.Fatalf("persona %s missing after concurrent access", id) } if m.Current() != Happy { t.Fatalf("persona %s Current() = %q, want %q", id, m.Current(), Happy) } } } func idOf(i int) string { return fmt.Sprintf("persona%d", i) }