package gateway import ( "context" "io" "log/slog" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // moodRunner 是一个 TurnRunner,以固定结果结算一轮,使测试能断言 // 被持久化的正是 loop 报告的心情。 type moodRunner struct { mood string } func (m moodRunner) Run(_ context.Context, _ string, _ []model.Message, _ loop.SessionEnv) (*loop.Result, error) { return &loop.Result{Text: "在的呀♪", Steps: 1, StopReason: "stop", Mood: m.mood}, nil } // TestOrchestratorPersistsTurnMood 在 Web 路径上端到端锁住 mood 列: // loop 轮次边界钩子报告的心情落到持久化的助手消息上 // (design/16 §3.3 `mode/mood/thinking`),因此 DTO、`message_persisted` // 与历史回放携带真实数据,而不是永远为空的心情。用户消息保持空 // 心情(design/16 §3.3 把心情分配给助手回复)。 func TestOrchestratorPersistsTurnMood(t *testing.T) { t.Parallel() hist := &fakeHistory{} o := NewOrchestrator(OrchestratorOptions{ Run: moodRunner{mood: "happy"}.Run, Log: slog.New(slog.NewTextHandler(io.Discard, nil)), History: hist, }) sr, res := o.SubmitSync(context.Background(), "web", []model.Message{userMsg("谢谢你")}, loop.OwnerEnv()) if !sr.Accepted || res == nil { t.Fatalf("submit = %+v res=%v, want accepted with a result", sr, res) } recs := hist.records() if len(recs) != 2 { t.Fatalf("appends = %d, want 2 (user + assistant)", len(recs)) } if recs[1].Role != history.RoleAssistant || recs[1].Mood != "happy" { t.Fatalf("assistant record = %+v, want Mood happy", recs[1]) } wire := toHistoryMessage(recs[1]) if wire.Mood != "happy" { t.Fatalf("wire mood = %q, want happy (message_persisted payload)", wire.Mood) } if recs[0].Mood != "" { t.Fatalf("user message mood = %q, want empty", recs[0].Mood) } } // TestOrchestratorPersistsEmptyMoodWhenUntracked 锁住诚实的降级:没有 // 情绪跟踪时(未接钩子,因此 Result.Mood 为空)存储的心情保持为空, // 而不是被编造。 func TestOrchestratorPersistsEmptyMoodWhenUntracked(t *testing.T) { t.Parallel() hist := &fakeHistory{} o := NewOrchestrator(OrchestratorOptions{ Run: moodRunner{mood: ""}.Run, Log: slog.New(slog.NewTextHandler(io.Discard, nil)), History: hist, }) if _, res := o.SubmitSync(context.Background(), "web", []model.Message{userMsg("你好")}, loop.OwnerEnv()); res == nil { t.Fatal("sync result is nil") } recs := hist.records() if len(recs) != 2 { t.Fatalf("appends = %d, want 2", len(recs)) } if recs[1].Mood != "" { t.Fatalf("assistant mood = %q, want empty", recs[1].Mood) } } // TestServerBroadcastMood 锁住前端 mood 处理器读取的 `mood` 帧 // (design/07 §3):类型是 `mood`,data.mood 是标签,角色 id / 好感度 // 作为额外细节一同携带。 func TestServerBroadcastMood(t *testing.T) { t.Parallel() env := newEnv(t, nil, false) c := newOwnerConn(env.srv.hub, 32) env.srv.hub.register(c) env.srv.BroadcastMood("cyrene", "happy", 60) ev := readFakeFrame(t, c) if ev.Type != "mood" { t.Fatalf("frame type = %q, want mood (frame=%+v)", ev.Type, ev) } data, ok := ev.Data.(map[string]any) if !ok || data["mood"] != "happy" || data["personaId"] != "cyrene" { t.Fatalf("frame data = %+v, want {mood:happy,personaId:cyrene}", ev.Data) } if got, ok := data["affection"].(float64); !ok || got != 60 { t.Fatalf("frame affection = %v, want 60", data["affection"]) } }