package loop import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // thinkingAdapter 发出一个完整的思考分段,后接一个文本分段, // 并记录它收到的每个请求,使测试能断言循环的 // 推理旋钮确实到达了模型调用。 type thinkingAdapter struct { reqs []model.Request } func (a *thinkingAdapter) Name() string { return "thinking-mock" } func (a *thinkingAdapter) Chat(_ context.Context, req model.Request, _ model.Options) (model.Response, error) { a.reqs = append(a.reqs, req) return model.Response{Content: "Hello"}, nil } func (a *thinkingAdapter) ChatStream(_ context.Context, req model.Request, on func(model.StreamEvent)) (model.Response, error) { a.reqs = append(a.reqs, req) events := []model.StreamEvent{ {Kind: model.StreamKindThinkingStart}, {Kind: model.StreamKindThinkingDelta, Delta: "let me think"}, {Kind: model.StreamKindThinkingEnd}, {Kind: model.StreamKindTextStart}, {Kind: model.StreamKindTextDelta, Delta: "Hello"}, {Kind: model.StreamKindTextEnd}, } for _, ev := range events { if on != nil { on(ev) } } return model.Response{Content: "Hello"}, nil } // kindsOf 把事件 kind 作为集合返回,便于紧凑断言。 func kindsOf(events []model.StreamEvent) map[model.StreamKind]int { out := make(map[model.StreamKind]int, len(events)) for _, ev := range events { out[ev.Kind]++ } return out } // runThinkingTurn 用给定广播开关配置的循环驱动一个流式 turn, // 并返回 OnStream 见过的事件。 func runThinkingTurn(t *testing.T, show *bool) ([]model.StreamEvent, *thinkingAdapter) { t.Helper() ad := &thinkingAdapter{} onStream, streams := collectStream() l := New(Options{ Agent: newTestAgent(t, ad, nil), OnStream: onStream, MaxRounds: 8, ThinkingShowToUser: show, }) if _, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("Run: %v", err) } return streams(), ad } // TestRun_ThinkingShowToUserFalseWithholdsThinkingFrames 是 showToUser=false // 的契约:思考帧绝不抵达客户端回调,而正文本 // 分段原样投递(展示被门控,答案不受影响)。 func TestRun_ThinkingShowToUserFalseWithholdsThinkingFrames(t *testing.T) { t.Parallel() show := false events, _ := runThinkingTurn(t, &show) kinds := kindsOf(events) for _, k := range []model.StreamKind{model.StreamKindThinkingStart, model.StreamKindThinkingDelta, model.StreamKindThinkingEnd} { if n := kinds[k]; n != 0 { t.Fatalf("event kind %q emitted %d time(s) with showToUser=false", k, n) } } for _, k := range []model.StreamKind{model.StreamKindTextStart, model.StreamKindTextDelta, model.StreamKindTextEnd} { if kinds[k] != 1 { t.Fatalf("event kind %q emitted %d time(s), want 1 (body must be unaffected)", k, kinds[k]) } } if len(events) != 3 { t.Fatalf("events = %d, want only the 3 text frames", len(events)) } } // TestRun_ThinkingShowToUserDefaultBroadcastsThinking 在选项未设置 // (nil)时保持既有行为:思考帧被推送。 func TestRun_ThinkingShowToUserDefaultBroadcastsThinking(t *testing.T) { t.Parallel() events, _ := runThinkingTurn(t, nil) kinds := kindsOf(events) if kinds[model.StreamKindThinkingDelta] != 1 || kinds[model.StreamKindThinkingStart] != 1 || kinds[model.StreamKindThinkingEnd] != 1 { t.Fatalf("thinking frames = %v, want start/delta/end each once with the option unset", kinds) } if len(events) != 6 { t.Fatalf("events = %d, want all 6 frames", len(events)) } } // TestRun_ThinkingShowToUserTrueBroadcastsThinking 断言显式 true // 的行为与默认值一致。 func TestRun_ThinkingShowToUserTrueBroadcastsThinking(t *testing.T) { t.Parallel() show := true events, _ := runThinkingTurn(t, &show) if kinds := kindsOf(events); kinds[model.StreamKindThinkingDelta] != 1 { t.Fatalf("thinking delta frames = %d, want 1 with showToUser=true", kinds[model.StreamKindThinkingDelta]) } } // TestRun_RequestCarriesThinkingKnobs 断言循环把它配置的推理 // 选项注入它构建的每个模型请求,使某人格的思考配置 // 真正被应用,而不是校验后被忽略。 func TestRun_RequestCarriesThinkingKnobs(t *testing.T) { t.Parallel() ad := &thinkingAdapter{} onStream, _ := collectStream() want := &model.Thinking{BudgetTokens: 4096} l := New(Options{ Agent: newTestAgent(t, ad, nil), OnStream: onStream, MaxRounds: 8, Thinking: want, ReasoningEffort: "low", }) if _, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "hi")}); err != nil { t.Fatalf("Run: %v", err) } if len(ad.reqs) == 0 { t.Fatal("no model request was recorded") } got := ad.reqs[0] if got.Thinking != want { t.Fatalf("request.Thinking = %+v, want %+v", got.Thinking, want) } if got.Thinking.BudgetTokens != 4096 { t.Fatalf("request.Thinking.BudgetTokens = %d, want 4096", got.Thinking.BudgetTokens) } if got.ReasoningEffort != "low" { t.Fatalf("request.ReasoningEffort = %q, want low", got.ReasoningEffort) } }