Files
ai-agent/internal/ai/openai_client_test.go
T
t 18c9354095 refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
2026-08-28 22:23:13 +08:00

125 lines
4.6 KiB
Go

package ai
import (
"context"
"io"
"net/http"
"strings"
"sync"
"testing"
"code.tczkiot.com/wlw/ai-agent/internal/models"
openai "github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/shared"
)
type requestIDRecordingTransport struct {
mu sync.Mutex
attempts int
requestIDs []string
}
func (t *requestIDRecordingTransport) RoundTrip(request *http.Request) (*http.Response, error) {
t.mu.Lock()
t.attempts++
attempt := t.attempts
t.requestIDs = append(t.requestIDs, request.Header.Get("X-AI-Request-ID"))
t.mu.Unlock()
status := http.StatusInternalServerError
body := `{"error":{"message":"retry","type":"server_error"}}`
if attempt > 1 {
status = http.StatusOK
body = `{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"platform-default","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`
}
return &http.Response{
StatusCode: status,
Status: http.StatusText(status),
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(body)),
Request: request,
}, nil
}
func TestPlatformOpenAIClientKeepsRequestIDAcrossRetries(t *testing.T) {
transport := &requestIDRecordingTransport{}
config := models.AIConfig{
APIKey: "platform-license",
BaseURL: "https://platform.example/v1",
ModelName: "platform-default",
MaxRetryCount: 1,
Platform: true,
HTTPClient: &http.Client{Transport: transport},
}
client := newOpenAIClient(config)
requestContext := WithPlatformAIRequestScope(context.Background(), "conversation:10:message:20:revision:30")
params := openai.ChatCompletionNewParams{
Model: shared.ChatModel("platform-default"),
Messages: []openai.ChatCompletionMessageParamUnion{{
OfUser: &openai.ChatCompletionUserMessageParam{
Content: openai.ChatCompletionUserMessageParamContentUnion{OfString: openai.String("hello")},
},
}},
}
_, err := client.Chat.Completions.New(
requestContext,
params,
platformRequestOptions(requestContext, config, "chat.completion")...,
)
if err != nil {
t.Fatalf("chat completion after retry: %v", err)
}
_, err = client.Chat.Completions.New(
requestContext,
params,
platformRequestOptions(requestContext, config, "chat.completion")...,
)
if err != nil {
t.Fatalf("second logical chat completion: %v", err)
}
transport.mu.Lock()
defer transport.mu.Unlock()
if transport.attempts != 3 {
t.Fatalf("attempts = %d, want 3", transport.attempts)
}
if transport.requestIDs[0] == "" || transport.requestIDs[0] != transport.requestIDs[1] {
t.Fatalf("request IDs = %q, want one stable non-empty ID", transport.requestIDs)
}
if transport.requestIDs[2] == "" || transport.requestIDs[2] == transport.requestIDs[0] {
t.Fatalf("request IDs = %q, want a fresh ID for the next logical call", transport.requestIDs)
}
}
func TestPlatformRequestIDsAreStableAcrossRecoveryAndSeparatePurposeAndOrdinal(t *testing.T) {
firstRun := WithPlatformAIRequestScope(context.Background(), "conversation:10:message:20:revision:30")
firstQuery := nextPlatformAIRequestID(WithPlatformAIRequestPurpose(firstRun, "embedding.knowledge-query"), "embedding")
secondQuery := nextPlatformAIRequestID(WithPlatformAIRequestPurpose(firstRun, "embedding.knowledge-query"), "embedding")
chat := nextPlatformAIRequestID(firstRun, "chat.completion")
recovered := WithPlatformAIRequestScope(context.Background(), "conversation:10:message:20:revision:30")
recoveredFirstQuery := nextPlatformAIRequestID(WithPlatformAIRequestPurpose(recovered, "embedding.knowledge-query"), "embedding")
recoveredSecondQuery := nextPlatformAIRequestID(WithPlatformAIRequestPurpose(recovered, "embedding.knowledge-query"), "embedding")
recoveredChat := nextPlatformAIRequestID(recovered, "chat.completion")
if firstQuery == secondQuery {
t.Fatalf("embedding ordinals collided: %q", firstQuery)
}
if firstQuery == chat {
t.Fatalf("embedding and chat purposes collided: %q", firstQuery)
}
if firstQuery != recoveredFirstQuery || secondQuery != recoveredSecondQuery || chat != recoveredChat {
t.Fatalf("recovery IDs changed: first=(%q,%q,%q) recovered=(%q,%q,%q)", firstQuery, secondQuery, chat, recoveredFirstQuery, recoveredSecondQuery, recoveredChat)
}
}
func TestCustomModelDoesNotReceivePlatformRequestOptions(t *testing.T) {
config := models.AIConfig{Platform: false}
ctx := WithPlatformAIRequestScope(context.Background(), "conversation:10:message:20:revision:30")
if options := platformRequestOptions(ctx, config, "embedding"); len(options) != 0 {
t.Fatalf("custom model options = %d, want 0", len(options))
}
}