Add typed jevdecide HTTP client (circuit, metrics) and coordinator DecideAssist wiring behind JEV_DECIDE_MODE (default off). Offline eval and synthetic soak evidence gate realtime/profile before assist rollout.
111 lines
3.9 KiB
Go
111 lines
3.9 KiB
Go
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())
|
|
}
|
|
}
|