Files
Cyrene_v2/internal/runtime/model/factory_test.go
T
AskaEth 3dc1232bc2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/model + internal/services/backthink(54 文件,纯替换)
- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
  改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
  `chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
  `rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
2026-10-04 16:27:54 +08:00

200 lines
6.4 KiB
Go

package model
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
func TestNewChatAdapter_Provider(t *testing.T) {
t.Parallel()
cases := []struct {
name string
provider string
wantType any
wantErr bool
}{
{"openai", ProviderOpenAI, (*OpenAIChatAdapter)(nil), false},
{"empty defaults to openai", "", (*OpenAIChatAdapter)(nil), false},
{"anthropic", ProviderAnthropic, (*AnthropicChatAdapter)(nil), false},
{"unknown", "gemini", nil, true},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
ad, err := NewChatAdapter(config.LLMConfig{Provider: c.provider, Model: "m", AnthropicModel: "m"})
if c.wantErr {
if !errors.Is(err, ErrUnknownProvider) {
t.Fatalf("err = %v, want ErrUnknownProvider", err)
}
if ad != nil {
t.Fatalf("adapter = %v, want nil on error", ad)
}
return
}
if err != nil {
t.Fatalf("NewChatAdapter error: %v", err)
}
switch c.wantType.(type) {
case *OpenAIChatAdapter:
if _, ok := ad.(*OpenAIChatAdapter); !ok {
t.Fatalf("adapter = %T, want *OpenAIChatAdapter", ad)
}
case *AnthropicChatAdapter:
if _, ok := ad.(*AnthropicChatAdapter); !ok {
t.Fatalf("adapter = %T, want *AnthropicChatAdapter", ad)
}
}
})
}
}
func TestNewChatChain_Provider(t *testing.T) {
t.Parallel()
t.Run("openai primary and fallback", func(t *testing.T) {
t.Parallel()
chain, err := NewChatChain(config.LLMConfig{Provider: ProviderOpenAI, BaseURL: "http://x", Model: "m", Fallback: "fb"})
if err != nil {
t.Fatalf("NewChatChain error: %v", err)
}
if len(chain.steps) != 2 {
t.Fatalf("steps = %d, want 2", len(chain.steps))
}
if chain.steps[0].Model != "m" || chain.steps[1].Model != "fb" {
t.Errorf("step models = [%s %s], want [m fb]", chain.steps[0].Model, chain.steps[1].Model)
}
if _, ok := chain.steps[0].Adapter.(*OpenAIChatAdapter); !ok {
t.Errorf("step[0] adapter = %T, want *OpenAIChatAdapter", chain.steps[0].Adapter)
}
})
t.Run("anthropic primary and fallback", func(t *testing.T) {
t.Parallel()
chain, err := NewChatChain(config.LLMConfig{
Provider: ProviderAnthropic,
AnthropicBaseURL: "http://x",
AnthropicModel: "claude-main",
AnthropicFallback: "claude-fb",
})
if err != nil {
t.Fatalf("NewChatChain error: %v", err)
}
if len(chain.steps) != 2 {
t.Fatalf("steps = %d, want 2", len(chain.steps))
}
if chain.steps[0].Model != "claude-main" || chain.steps[1].Model != "claude-fb" {
t.Errorf("step models = [%s %s], want [claude-main claude-fb]", chain.steps[0].Model, chain.steps[1].Model)
}
if _, ok := chain.steps[0].Adapter.(*AnthropicChatAdapter); !ok {
t.Errorf("step[0] adapter = %T, want *AnthropicChatAdapter", chain.steps[0].Adapter)
}
})
t.Run("missing model returns ErrNoModel", func(t *testing.T) {
t.Parallel()
if _, err := NewChatChain(config.LLMConfig{Provider: ProviderOpenAI, BaseURL: "http://x"}); !errors.Is(err, ErrNoModel) {
t.Fatalf("openai err = %v, want ErrNoModel", err)
}
if _, err := NewChatChain(config.LLMConfig{Provider: ProviderAnthropic, AnthropicBaseURL: "http://x"}); !errors.Is(err, ErrNoModel) {
t.Fatalf("anthropic err = %v, want ErrNoModel", err)
}
})
t.Run("unknown provider returns ErrUnknownProvider", func(t *testing.T) {
t.Parallel()
if _, err := NewChatChain(config.LLMConfig{Provider: "gemini"}); !errors.Is(err, ErrUnknownProvider) {
t.Fatalf("err = %v, want ErrUnknownProvider", err)
}
})
}
func TestNewOpenAIChatChain_StillWorks(t *testing.T) {
t.Parallel()
chain, err := NewOpenAIChatChain(config.LLMConfig{BaseURL: "http://x", Model: "m", Fallback: "fb"})
if err != nil {
t.Fatalf("NewOpenAIChatChain error: %v", err)
}
if len(chain.steps) != 2 {
t.Fatalf("steps = %d, want 2", len(chain.steps))
}
if _, ok := chain.steps[0].Adapter.(*OpenAIChatAdapter); !ok {
t.Errorf("step[0] adapter = %T, want *OpenAIChatAdapter", chain.steps[0].Adapter)
}
}
func TestDefaultModelForProvider(t *testing.T) {
t.Parallel()
if got := DefaultModelForProvider(config.LLMConfig{Provider: ProviderOpenAI, Model: "gpt"}); got != "gpt" {
t.Errorf("openai = %q, want gpt", got)
}
if got := DefaultModelForProvider(config.LLMConfig{Provider: ProviderAnthropic, AnthropicModel: "claude"}); got != "claude" {
t.Errorf("anthropic = %q, want claude", got)
}
}
func TestRouter_RouteProvider(t *testing.T) {
t.Parallel()
r := NewRouter("default",
WithDefaultProvider(ProviderAnthropic),
WithRoleProvider(Role("engineer"), ProviderOpenAI),
)
cases := []struct {
role Role
want string
}{
{Role("engineer"), ProviderOpenAI},
{Role("user"), ProviderAnthropic},
{Role("assistant"), ProviderAnthropic},
}
for _, c := range cases {
c := c
t.Run(string(c.role), func(t *testing.T) {
t.Parallel()
if got := r.RouteProvider(c.role); got != c.want {
t.Fatalf("RouteProvider(%q) = %q, want %q", c.role, got, c.want)
}
})
}
}
// TestChain_AnthropicFallback 用两个 Anthropic 适配器验证
// 降级链:主步骤失败、备用步骤成功,证明该
// 适配器能在 Chain 内干净地组合。
func TestChain_AnthropicFallback(t *testing.T) {
t.Parallel()
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = io.WriteString(w, `{"error":"down"}`)
}))
t.Cleanup(primary.Close)
backup := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, `{"id":"msg_b","model":"backup","content":[{"type":"text","text":"Hello!"}],"usage":{"input_tokens":5,"output_tokens":2}}`)
}))
t.Cleanup(backup.Close)
chain := NewChain(
Step{Model: "main", Adapter: NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: primary.URL, AnthropicModel: "main"})},
Step{Model: "backup", Adapter: NewAnthropicChat(config.LLMConfig{AnthropicBaseURL: backup.URL, AnthropicModel: "backup"})},
)
resp, err := chain.Chat(context.Background(),
Request{Messages: []Message{TextMessage(RoleUser, "hi")}, Purpose: "chat"}, Options{})
if err != nil {
t.Fatalf("Chain.Chat error: %v", err)
}
if resp.Content != "Hello!" {
t.Errorf("resp.Content = %q, want Hello!", resp.Content)
}
if resp.Model != "backup" {
t.Errorf("resp.Model = %q, want backup", resp.Model)
}
}