18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
125 lines
4.6 KiB
Go
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))
|
|
}
|
|
}
|