package emotion import "testing" // TestReactClassifiesTurns locks the turn reading the observer applies: anger and // negative cues outrank positive ones, strong warmth outranks mild warmth, and a // cue-free turn carries no signal (so the machine is left to decay, not jump). func TestReactClassifiesTurns(t *testing.T) { t.Parallel() cases := []struct { name string text string state EmotionState delta int sig bool }{ {name: "empty", text: "", sig: false}, {name: "plain task", text: "帮我把这个文件重命名一下", sig: false}, {name: "mild warm", text: "今天谢谢你呀", state: Happy, delta: affectionUp, sig: true}, {name: "greeting", text: "早上好", state: Happy, delta: affectionUp, sig: true}, {name: "strong warm", text: "我太开心了,最喜欢你了", state: Excited, delta: affectionUp, sig: true}, {name: "down", text: "今天有点难过", state: Sad, delta: affectionDown, sig: true}, {name: "tired", text: "我好累", state: Sad, delta: affectionDown, sig: true}, {name: "angry", text: "别烦我了", state: Angry, delta: affectionDown, sig: true}, // "不开心" contains "开心": the negative reading must win, otherwise a // complaint would raise the mood. {name: "negated positive", text: "我不开心", state: Sad, delta: affectionDown, sig: true}, {name: "ascii warm", text: "thanks, miss you", state: Happy, delta: affectionUp, sig: true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() got := React(tc.text) if got.Signal != tc.sig { t.Fatalf("React(%q).Signal = %v, want %v", tc.text, got.Signal, tc.sig) } if !tc.sig { return } if got.State != tc.state || got.AffectionDelta != tc.delta { t.Fatalf("React(%q) = {%s, %d}, want {%s, %d}", tc.text, got.State, got.AffectionDelta, tc.state, tc.delta) } }) } } // TestMachineMoveWalksTransitions verifies Move honors the installed transition // table instead of forcing a state: an installed edge is taken directly, and an // emotion two edges away is reached through the legal intermediate (the default // table has no calm→angry or calm→excited edge). func TestMachineMoveWalksTransitions(t *testing.T) { t.Parallel() m, err := NewMachine(Params{InitialState: Calm}) if err != nil { t.Fatalf("NewMachine: %v", err) } if got := m.Move(Happy); got != Happy { t.Fatalf("calm → happy = %s, want direct happy", got) } m.Set(Calm) if got := m.Move(Angry); got != Sad { t.Fatalf("calm → angry = %s, want the legal intermediate sad", got) } if got := m.Move(Angry); got != Angry { t.Fatalf("sad → angry = %s, want direct angry", got) } m.Set(Calm) if got := m.Move(Excited); got != Happy { t.Fatalf("calm → excited = %s, want the legal intermediate happy", got) } // A target with no legal path leaves the machine where it was. p, err := NewMachine(Params{ InitialState: Calm, Transitions: map[EmotionState][]EmotionState{Calm: {Happy}}, }) if err != nil { t.Fatalf("NewMachine: %v", err) } if got := p.Move(Sad); got != Calm { t.Fatalf("unreachable move = %s, want calm unchanged", got) } if got := m.Move(""); got != m.Current() { t.Fatalf("empty target moved to %s, want unchanged", got) } } // TestMachineMoveHonorsCustomTable verifies a persona's own transition table wins: // a custom edge that skips the built-in intermediate is taken directly. func TestMachineMoveHonorsCustomTable(t *testing.T) { t.Parallel() m, err := NewMachine(Params{ InitialState: Calm, Transitions: map[EmotionState][]EmotionState{ Calm: {Excited}, Excited: {Calm}, }, }) if err != nil { t.Fatalf("NewMachine: %v", err) } if got := m.Move(Excited); got != Excited { t.Fatalf("custom calm → excited = %s, want direct excited", got) } }