2026-08-26 06:41:07 +08:00
|
|
|
package orchestration
|
|
|
|
|
|
|
|
|
|
import (
|
2026-08-29 21:56:51 +08:00
|
|
|
"encoding/json"
|
2026-08-26 06:41:07 +08:00
|
|
|
"errors"
|
2026-08-29 21:56:51 +08:00
|
|
|
"strings"
|
2026-08-26 06:41:07 +08:00
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestConstructorAndBoundaryValidation(t *testing.T) {
|
|
|
|
|
if _, err := NewStartProcessor(nil, nil, nil, 0); err == nil {
|
|
|
|
|
t.Fatal("invalid start processor dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if err := (&StartProcessor{}).ConfigureStartProtection(nil, 0); err == nil {
|
|
|
|
|
t.Fatal("invalid provider start protection was accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewStopProcessor(nil, nil, nil); err == nil {
|
|
|
|
|
t.Fatal("invalid stop processor dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewProviderEventProcessor(nil, 0); err == nil {
|
|
|
|
|
t.Fatal("invalid provider event processor dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewProviderHealth(nil, 0, 0); err == nil {
|
|
|
|
|
t.Fatal("invalid provider health dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewHeartbeatService(nil, nil, nil, nil, HeartbeatServiceOptions{}); err == nil {
|
|
|
|
|
t.Fatal("invalid heartbeat dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewRecoveryScanner(nil, nil, nil, RecoveryOptions{}); err == nil {
|
|
|
|
|
t.Fatal("invalid recovery scanner dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewTranscriptBatchService(nil, nil, nil, nil); err == nil {
|
|
|
|
|
t.Fatal("invalid transcript dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewService(nil, nil, nil, nil, nil, nil, ServiceOptions{}); err == nil {
|
|
|
|
|
t.Fatal("invalid orchestration dependencies were accepted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conflict := &TranscriptConflictError{}
|
|
|
|
|
if !errors.Is(conflict, conflict) || conflict.Error() == "" {
|
|
|
|
|
t.Fatal("transcript conflict error contract is invalid")
|
|
|
|
|
}
|
|
|
|
|
if got := truncateContextText(" 长者今日精神状态良好 ", 4); got != "长者今日…" {
|
|
|
|
|
t.Fatalf("truncated context=%q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := truncateContextText(" 简短 ", 10); got != "简短" {
|
|
|
|
|
t.Fatalf("short context=%q", got)
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewRedisLeaseCache(nil, "test"); err == nil {
|
|
|
|
|
t.Fatal("nil redis client was accepted")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAuthorizedProfileContextRejectsMalformedWhitelistedFields(t *testing.T) {
|
|
|
|
|
valid, err := authorizedProfileContext(
|
|
|
|
|
[]byte(`{"background":"退休教师","interests":["戏曲","散步"],"internal_note":{"ignored":true}}`),
|
2026-08-29 21:56:51 +08:00
|
|
|
[]byte(`{"conversation_goal":"轻松交流","pace":"slow","preferred_topics":["家乡"],"avoided_topics":["收入","家庭矛盾"]}`),
|
2026-08-26 06:41:07 +08:00
|
|
|
)
|
2026-08-29 21:56:51 +08:00
|
|
|
if err != nil || valid.Background != "退休教师" || len(valid.Interests) != 2 || valid.Pace != "slow" || len(valid.AvoidedTopics) != 2 {
|
2026-08-26 06:41:07 +08:00
|
|
|
t.Fatalf("valid context=%+v err=%v", valid, err)
|
|
|
|
|
}
|
|
|
|
|
for _, malformed := range []struct {
|
|
|
|
|
profile, preferences string
|
|
|
|
|
}{
|
|
|
|
|
{`{"background":42}`, `{}`},
|
|
|
|
|
{`{"interests":["戏曲",42]}`, `{}`},
|
|
|
|
|
{`{}`, `{"preferred_topics":"家乡"}`},
|
2026-08-29 21:56:51 +08:00
|
|
|
{`{}`, `{"avoided_topics":"收入"}`},
|
2026-08-26 06:41:07 +08:00
|
|
|
} {
|
|
|
|
|
if _, err := authorizedProfileContext([]byte(malformed.profile), []byte(malformed.preferences)); err == nil {
|
|
|
|
|
t.Fatalf("malformed whitelisted context was accepted: profile=%s preferences=%s", malformed.profile, malformed.preferences)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-29 21:56:51 +08:00
|
|
|
|
|
|
|
|
func TestBuildContextSummaryIncludesContinuityAndBoundariesWithinSnapshotLimit(t *testing.T) {
|
|
|
|
|
profile := authorizedSubjectContext{
|
|
|
|
|
Background: "退休教师", Interests: []string{"戏曲"}, ConversationGoal: "自然陪伴", Pace: "slow",
|
|
|
|
|
PreferredTopics: []string{"家乡"}, AvoidedTopics: []string{"收入", "家庭矛盾"},
|
|
|
|
|
}
|
|
|
|
|
result := buildContextSummary("王阿姨", profile,
|
|
|
|
|
[]string{"上次聊到刚刚注册了个体工商户,准备开始新的工作。"},
|
|
|
|
|
[]string{"喜欢在周末听越剧。"})
|
|
|
|
|
for _, expected := range []string{"交流边界", "收入", "王阿姨", "最近第1次通话摘要", "注册了个体工商户", "已人工确认的长期记忆", "越剧"} {
|
|
|
|
|
if !strings.Contains(result, expected) {
|
|
|
|
|
t.Fatalf("context %q does not contain %q", result, expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
oversized := buildContextSummary(strings.Repeat("长", 120), authorizedSubjectContext{
|
|
|
|
|
Background: strings.Repeat("背", 500), AvoidedTopics: []string{strings.Repeat("禁", 50), "家庭矛盾"},
|
|
|
|
|
PreferredTopics: []string{strings.Repeat("优", 50)}, Interests: []string{strings.Repeat("兴", 50)},
|
|
|
|
|
}, []string{strings.Repeat("摘", 500), "第二次摘要", "第三次摘要"}, []string{strings.Repeat("忆", 500), "第二条记忆", "第三条记忆", "第四条记忆", "第五条记忆"})
|
|
|
|
|
if len([]rune(oversized)) > contextSummaryLimit {
|
|
|
|
|
t.Fatalf("context length=%d exceeds snapshot budget", len([]rune(oversized)))
|
|
|
|
|
}
|
|
|
|
|
for _, expected := range []string{"交流边界", "家庭矛盾", "最近第3次通话摘要", "第三次摘要", "已人工确认的长期记忆", "第五条记忆"} {
|
|
|
|
|
if !strings.Contains(oversized, expected) {
|
|
|
|
|
t.Fatalf("oversized context %q lost priority field %q", oversized, expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestConversationContinuityRequiresBothExplicitConsents(t *testing.T) {
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
|
value json.RawMessage
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{json.RawMessage(`{"personalized_memory":true,"story_extraction":true}`), true},
|
|
|
|
|
{json.RawMessage(`{"personalized_memory":true}`), false},
|
|
|
|
|
{json.RawMessage(`{"story_extraction":true}`), false},
|
|
|
|
|
{json.RawMessage(`not-json`), false},
|
|
|
|
|
} {
|
|
|
|
|
if got := consentAllowsConversationContinuity(test.value); got != test.want {
|
|
|
|
|
t.Fatalf("consent %s allowed=%v want=%v", test.value, got, test.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|