package coordinator import ( "context" "errors" "testing" "chat-agent-system/internal/jevdecide" ) type stubDecide struct { yes bool bucket string ans jevdecide.Answer err error } func (s *stubDecide) DecideRealtime(context.Context, string) (bool, jevdecide.Answer, error) { return s.yes, s.ans, s.err } func (s *stubDecide) DecideProfileBucket(context.Context, string) (string, jevdecide.Answer, error) { return s.bucket, s.ans, s.err } func TestResolveRealtimeQuery_ModeOffIgnoresJev(t *testing.T) { jevdecide.SetModeForTest(jevdecide.ModeOff) defer jevdecide.ResetModeForTest() e := &CoordinatorEngine{decideAssist: &stubDecide{yes: true, ans: jevdecide.Answer{Choice: "yes", Confidence: 0.99}}} // business lexicon → rule false; jev says yes but mode off → false if e.resolveRealtimeQuery(context.Background(), "型检温度指标怎么定") { t.Fatal("mode=off must keep rule false") } // weather → rule true if !e.resolveRealtimeQuery(context.Background(), "今天北京天气怎么样") { t.Fatal("mode=off must keep rule true") } } func TestResolveRealtimeQuery_ShadowDoesNotChangeRule(t *testing.T) { jevdecide.SetModeForTest(jevdecide.ModeShadow) defer jevdecide.ResetModeForTest() jevdecide.Global.Reset() e := &CoordinatorEngine{decideAssist: &stubDecide{ yes: true, ans: jevdecide.Answer{Choice: "yes", Confidence: 0.99, Probabilities: map[string]float64{"yes": 0.99, "no": 0.01}}, }} got := e.resolveRealtimeQuery(context.Background(), "项目进度怎么样") if got { t.Fatal("shadow must not flip rule false→true") } if jevdecide.Global.Disagree.Load() != 1 { t.Fatalf("expected disagree recorded, got %+v", jevdecide.Global.Snapshot()) } } func TestResolveRealtimeQuery_AssistAddsRecallOnly(t *testing.T) { jevdecide.SetModeForTest(jevdecide.ModeAssist) defer jevdecide.ResetModeForTest() jevdecide.Global.Reset() e := &CoordinatorEngine{decideAssist: &stubDecide{ yes: true, ans: jevdecide.Answer{Choice: "yes", Yes: true, Confidence: 0.96, Probabilities: map[string]float64{"yes": 0.96, "no": 0.04}}, }} // paraphrase weather without marker — rule false, assist may add if !e.resolveRealtimeQuery(context.Background(), "外面冷不冷啊今天") { t.Fatal("assist high-conf yes should add recall") } if jevdecide.Global.AssistHit.Load() != 1 { t.Fatal("expected assist hit") } // assist must not suppress rule true e2 := &CoordinatorEngine{decideAssist: &stubDecide{ yes: false, ans: jevdecide.Answer{Choice: "no", Confidence: 0.99, Probabilities: map[string]float64{"no": 0.99, "yes": 0.01}}, }} if !e2.resolveRealtimeQuery(context.Background(), "今天北京天气怎么样") { t.Fatal("assist must not turn rule true into false") } } func TestResolveRealtimeQuery_FailOpen(t *testing.T) { jevdecide.SetModeForTest(jevdecide.ModeAssist) defer jevdecide.ResetModeForTest() e := &CoordinatorEngine{decideAssist: &stubDecide{err: errors.New("down")}} if e.resolveRealtimeQuery(context.Background(), "今天北京天气怎么样") != true { t.Fatal("fail-open must keep rule true") } if e.resolveRealtimeQuery(context.Background(), "项目进度怎么样") != false { t.Fatal("fail-open must keep rule false") } } func TestShadowProfile_RecordsWithoutChanging(t *testing.T) { jevdecide.SetModeForTest(jevdecide.ModeShadow) defer jevdecide.ResetModeForTest() jevdecide.Global.Reset() e := &CoordinatorEngine{decideAssist: &stubDecide{ bucket: "high", ans: jevdecide.Answer{Choice: "high", Confidence: 0.8, Probabilities: map[string]float64{"high": 0.8, "mid": 0.1, "low": 0.1}}, }} profile := ProfileTask("你好", nil) e.shadowProfile(context.Background(), "你好", profile) if jevdecide.ProfileBucket(profile.ComplexityScore) != "low" { t.Fatal("rule bucket should stay low") } if jevdecide.Global.Disagree.Load() != 1 { t.Fatalf("expected disagree, got %+v", jevdecide.Global.Snapshot()) } }