package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "testing" "time" "git.x2erp.com/qdy/go-svc-code/internal/opencode" ) // TestDirectOpenCodeMessages 直接调用opencode API (8787端口) 获取会话消息 func TestDirectOpenCodeMessages(t *testing.T) { // 外部已启动的OpenCode服务端口 externalOpenCodePort := 8787 opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort) // 检查OpenCode服务是否运行 if !isServiceRunningForMessages(t, opencodeURL) { t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL) } t.Logf("🚀 开始测试直接调用OpenCode API获取消息") t.Logf("OpenCode URL: %s", opencodeURL) // 使用现有的测试会话ID sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" t.Logf("测试会话ID: %s", sessionID) // 1. 使用DirectClient调用(测试SDK接口) t.Run("SDKClient", func(t *testing.T) { testDirectClientMessages(t, externalOpenCodePort, sessionID) }) // 2. 直接HTTP调用(测试原始API) t.Run("RawHTTP", func(t *testing.T) { testRawHTTPMessages(t, opencodeURL, sessionID) }) } // testDirectClientMessages 使用DirectClient测试获取消息 func testDirectClientMessages(t *testing.T, port int, sessionID string) { // 创建客户端 client, err := opencode.NewDirectClient(port) if err != nil { t.Fatalf("❌ 创建OpenCode客户端失败: %v", err) } t.Logf("✅ 创建OpenCode客户端成功,基础URL: %s", client.GetBaseURL()) ctx := context.Background() // 测试不带limit参数 t.Log("📝 测试获取消息(不带limit)") messages, err := client.GetSessionMessages(ctx, sessionID, 0) if err != nil { t.Errorf("❌ 获取会话消息失败: %v", err) return } t.Logf("✅ 获取到 %d 条消息", len(messages)) // 打印消息摘要 for i, msg := range messages { t.Logf(" 消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg)) if i < 3 && len(messages) > 3 { // 只打印前3条 if content := extractMessageContent(msg); content != "" { t.Logf(" 内容预览: %.100s...", content) } } } // 测试带limit参数 t.Log("📝 测试获取消息(limit=5)") messagesLimited, err := client.GetSessionMessages(ctx, sessionID, 5) if err != nil { t.Errorf("❌ 获取限制数量的会话消息失败: %v", err) return } t.Logf("✅ 获取到 %d 条消息(限制5条)", len(messagesLimited)) if len(messages) > 0 && len(messagesLimited) > 0 { if len(messagesLimited) > 5 { t.Errorf("❌ 消息数量超过限制: %d > 5", len(messagesLimited)) } } } // testRawHTTPMessages 直接HTTP调用测试获取消息 func testRawHTTPMessages(t *testing.T, baseURL, sessionID string) { // 构造URL url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID) t.Logf("📡 调用原始API: %s", url) // 发送HTTP请求 client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("❌ 创建请求失败: %v", err) } req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { t.Fatalf("❌ HTTP请求失败: %v", err) } defer resp.Body.Close() t.Logf("📊 响应状态码: %d", resp.StatusCode) if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Fatalf("❌ 获取会话消息失败,状态码: %d, 响应体: %s", resp.StatusCode, string(body)) } // 解析响应 body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("❌ 读取响应体失败: %v", err) } // 尝试解析为SessionMessage数组 var messages []opencode.SessionMessage if err := json.Unmarshal(body, &messages); err != nil { t.Errorf("❌ 解析消息响应失败: %v", err) t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))])) return } t.Logf("✅ 解析成功,获取到 %d 条消息", len(messages)) // 验证消息结构 for i, msg := range messages { if extractMessageID(msg) == "" { t.Errorf("❌ 消息[%d]缺少ID", i) } if extractMessageRole(msg) == "" { t.Errorf("❌ 消息[%d]缺少角色", i) } } // 测试带limit参数 urlWithLimit := fmt.Sprintf("%s?limit=3", url) t.Logf("📡 调用带limit的API: %s", urlWithLimit) reqLimit, err := http.NewRequest("GET", urlWithLimit, nil) if err != nil { t.Errorf("❌ 创建带limit的请求失败: %v", err) return } reqLimit.Header.Set("Accept", "application/json") respLimit, err := client.Do(reqLimit) if err != nil { t.Errorf("❌ HTTP请求(带limit)失败: %v", err) return } defer respLimit.Body.Close() if respLimit.StatusCode == http.StatusOK { bodyLimit, _ := io.ReadAll(respLimit.Body) var messagesLimit []opencode.SessionMessage if err := json.Unmarshal(bodyLimit, &messagesLimit); err == nil { t.Logf("✅ 带limit获取到 %d 条消息", len(messagesLimit)) if len(messagesLimit) > 3 { t.Errorf("❌ 带limit的消息数量超过限制: %d > 3", len(messagesLimit)) } } } } // TestSvcCodeMessagesAPI 通过svc-code API (8020端口) 获取会话消息 func TestSvcCodeMessagesAPI(t *testing.T) { // svc-code服务地址 svcCodeURL := "http://localhost:8020" // 检查svc-code服务是否运行 if !isServiceRunningForMessages(t, svcCodeURL) { t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL) } t.Logf("🚀 开始测试通过svc-code API获取消息") t.Logf("svc-code URL: %s", svcCodeURL) // 使用现有的测试会话ID sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" t.Logf("测试会话ID: %s", sessionID) // 1. 用户登录获取token token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL) if err != nil { t.Fatalf("❌ 登录失败: %v", err) } t.Logf("✅ 获取到Token: %s...", token[:minForMessagesTest(20, len(token))]) // 2. 测试不带limit参数 t.Run("WithoutLimit", func(t *testing.T) { testSvcCodeMessages(t, svcCodeURL, token, sessionID, 0) }) // 3. 测试带limit参数 t.Run("WithLimit", func(t *testing.T) { testSvcCodeMessages(t, svcCodeURL, token, sessionID, 10) }) // 4. 测试无效token t.Run("InvalidToken", func(t *testing.T) { testInvalidTokenForMessages(t, svcCodeURL, sessionID) }) // 5. 测试无效sessionID t.Run("InvalidSessionID", func(t *testing.T) { testInvalidSessionIDForMessages(t, svcCodeURL, token) }) } // testSvcCodeMessages 测试svc-code获取消息API func testSvcCodeMessages(t *testing.T, baseURL, token, sessionID string, limit int) { // 构造URL url := fmt.Sprintf("%s/api/session/messages", baseURL) // 创建请求 req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("❌ 创建请求失败: %v", err) } // 添加查询参数 q := req.URL.Query() q.Add("sessionID", sessionID) if limit > 0 { q.Add("limit", fmt.Sprintf("%d", limit)) } req.URL.RawQuery = q.Encode() // 添加认证头 req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") t.Logf("📡 调用svc-code API: %s", req.URL.String()) // 发送请求 client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("❌ HTTP请求失败: %v", err) } defer resp.Body.Close() t.Logf("📊 响应状态码: %d", resp.StatusCode) body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("❌ 读取响应体失败: %v", err) } // 解析响应 var result struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Messages []opencode.SessionMessage `json:"messages"` Count int `json:"count"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { t.Errorf("❌ 解析响应失败: %v", err) t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))])) return } if !result.Success { t.Errorf("❌ API调用失败: %s", result.Message) return } t.Logf("✅ 获取成功!消息数量: %d, 总计: %d", len(result.Data.Messages), result.Data.Count) // 验证数据一致性 if result.Data.Count != len(result.Data.Messages) { t.Errorf("❌ 数据不一致: Count=%d, Messages长度=%d", result.Data.Count, len(result.Data.Messages)) } // 打印消息摘要 for i, msg := range result.Data.Messages { t.Logf(" 消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg)) if i < 2 && len(result.Data.Messages) > 2 { if content := extractMessageContent(msg); content != "" { t.Logf(" 内容预览: %.100s...", content) } } } // 验证limit参数效果 if limit > 0 && len(result.Data.Messages) > limit { t.Errorf("❌ 消息数量超过限制: %d > %d", len(result.Data.Messages), limit) } } // testInvalidTokenForMessages 测试无效token func testInvalidTokenForMessages(t *testing.T, baseURL, sessionID string) { url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, sessionID) req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("创建请求失败: %v", err) } // 使用无效token req.Header.Set("Authorization", "Bearer invalid_token_12345") client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("HTTP请求失败: %v", err) } defer resp.Body.Close() // 应该返回401或403 if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden { t.Errorf("预期认证错误状态码(401/403),实际: %d", resp.StatusCode) body, _ := io.ReadAll(resp.Body) t.Logf("响应体: %s", string(body)) } else { t.Logf("✅ 无效token测试通过,返回状态码: %d", resp.StatusCode) } } // testInvalidSessionIDForMessages 测试无效sessionID func testInvalidSessionIDForMessages(t *testing.T, baseURL, token string) { invalidSessionID := "ses_invalid_12345" url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, invalidSessionID) req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatalf("创建请求失败: %v", err) } req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("HTTP请求失败: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("读取响应体失败: %v", err) } var result struct { Success bool `json:"success"` Message string `json:"message"` } if err := json.Unmarshal(body, &result); err != nil { t.Errorf("解析响应失败: %v", err) return } if result.Success { t.Errorf("无效sessionID应该返回失败,实际成功") } else { t.Logf("✅ 无效sessionID测试通过,错误信息: %s", result.Message) } } // loginAndGetTokenForMessagesTest 登录获取token func loginAndGetTokenForMessagesTest(t *testing.T, baseURL string) (string, error) { loginURL := baseURL + "/api/auth/login" loginData := map[string]string{ "user_id": "test-user-001", "password": "password123", } jsonData, _ := json.Marshal(loginData) resp, err := http.Post(loginURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("登录请求失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("登录失败 (状态码 %d): %s", resp.StatusCode, string(bodyBytes)) } var result struct { Success bool `json:"success"` Data string `json:"data"` Message string `json:"message"` } bodyBytes, _ := io.ReadAll(resp.Body) if err := json.Unmarshal(bodyBytes, &result); err != nil { return "", fmt.Errorf("解析响应失败: %v", err) } if !result.Success { return "", fmt.Errorf("登录失败: %s", result.Message) } return result.Data, nil } // isServiceRunningForMessages 检查服务是否运行 func isServiceRunningForMessages(t *testing.T, url string) bool { client := &http.Client{Timeout: 3 * time.Second} // 尝试多个端点 endpoints := []string{ "/global/health", // opencode健康检查 "/api/auth/health", // svc-code健康检查 "/api/health", // 其他健康检查 "", // 根路径 } for _, endpoint := range endpoints { fullURL := url + endpoint resp, err := client.Get(fullURL) if err == nil { resp.Body.Close() if resp.StatusCode == http.StatusOK { t.Logf("✅ 服务运行正常: %s (状态码: %d)", fullURL, resp.StatusCode) return true } } } return false } // extractMessageID 从SessionMessage提取消息ID func extractMessageID(msg opencode.SessionMessage) string { if msg.Info != nil { if id, ok := msg.Info["id"].(string); ok { return id } } return "" } // extractMessageRole 从SessionMessage提取消息角色 func extractMessageRole(msg opencode.SessionMessage) string { if msg.Info != nil { if role, ok := msg.Info["role"].(string); ok { return role } } return "" } // extractMessageContent 从SessionMessage提取消息内容 func extractMessageContent(msg opencode.SessionMessage) string { if len(msg.Parts) > 0 { for _, part := range msg.Parts { if text, ok := part["text"].(string); ok && text != "" { return text } } } return "" } // minForMessagesTest 返回两个整数的最小值 func minForMessagesTest(a, b int) int { if a < b { return a } return b } // extractTimestamp 从SessionMessage提取时间戳 func extractTimestamp(msg opencode.SessionMessage) (time.Time, error) { // 尝试从info.time.created提取 if msg.Info != nil { // 调试:打印info的所有键 // fmt.Printf("Info keys: %v\n", getMapKeys(msg.Info)) // 1. 尝试从time.created提取(毫秒时间戳) if timeMap, ok := msg.Info["time"].(map[string]interface{}); ok { // fmt.Printf("Time map keys: %v\n", getMapKeys(timeMap)) // 尝试毫秒时间戳(整数) if createdMs, ok := timeMap["created"].(float64); ok { // 转换为秒(除以1000) seconds := int64(createdMs) / 1000 nanoseconds := (int64(createdMs) % 1000) * 1000000 return time.Unix(seconds, nanoseconds), nil } // 尝试字符串格式 if createdStr, ok := timeMap["created"].(string); ok { return time.Parse(time.RFC3339, createdStr) } } // 2. 尝试直接提取created(毫秒时间戳) if createdMs, ok := msg.Info["created"].(float64); ok { seconds := int64(createdMs) / 1000 nanoseconds := (int64(createdMs) % 1000) * 1000000 return time.Unix(seconds, nanoseconds), nil } // 3. 尝试从created_at提取(字符串或数字) if createdVal, ok := msg.Info["created_at"]; ok { switch v := createdVal.(type) { case float64: seconds := int64(v) / 1000 nanoseconds := (int64(v) % 1000) * 1000000 return time.Unix(seconds, nanoseconds), nil case string: return time.Parse(time.RFC3339, v) } } // 4. 尝试从createdAt提取(驼峰式) if createdAtVal, ok := msg.Info["createdAt"]; ok { switch v := createdAtVal.(type) { case float64: seconds := int64(v) / 1000 nanoseconds := (int64(v) % 1000) * 1000000 return time.Unix(seconds, nanoseconds), nil case string: return time.Parse(time.RFC3339, v) } } // 5. 尝试从timestamp提取 if timestampFloat, ok := msg.Info["timestamp"].(float64); ok { return time.Unix(int64(timestampFloat), 0), nil } // 6. 尝试从date提取 if dateStr, ok := msg.Info["date"].(string); ok { return time.Parse(time.RFC3339, dateStr) } // 7. 尝试从time直接提取(可能是字符串) if timeStr, ok := msg.Info["time"].(string); ok { return time.Parse(time.RFC3339, timeStr) } } return time.Time{}, fmt.Errorf("无法提取时间戳") } // getMapKeys 获取map的所有键(用于调试) func getMapKeys(m map[string]interface{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // TestMessageFormatAndSorting 测试消息格式和排序规则 func TestMessageFormatAndSorting(t *testing.T) { // 外部已启动的OpenCode服务端口 externalOpenCodePort := 8787 opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort) // 检查OpenCode服务是否运行 if !isServiceRunningForMessages(t, opencodeURL) { t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL) } t.Logf("🚀 开始测试消息格式和排序规则") t.Logf("OpenCode URL: %s", opencodeURL) // 使用现有的测试会话ID sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" t.Logf("测试会话ID: %s", sessionID) // 创建客户端 client, err := opencode.NewDirectClient(externalOpenCodePort) if err != nil { t.Fatalf("❌ 创建OpenCode客户端失败: %v", err) } ctx := context.Background() // 获取消息(不带limit) t.Log("📝 获取消息进行格式和排序分析") messages, err := client.GetSessionMessages(ctx, sessionID, 0) if err != nil { t.Fatalf("❌ 获取会话消息失败: %v", err) } t.Logf("✅ 获取到 %d 条消息进行分析", len(messages)) if len(messages) == 0 { t.Skip("会话中没有消息,跳过格式和排序测试") } // 1. 验证消息格式完整性 t.Run("MessageFormatIntegrity", func(t *testing.T) { for i, msg := range messages { // 检查消息ID msgID := extractMessageID(msg) if msgID == "" { t.Errorf("❌ 消息[%d]缺少ID", i) } // 检查角色 role := extractMessageRole(msg) if role == "" { t.Errorf("❌ 消息[%d]缺少角色", i) } // 检查内容 content := extractMessageContent(msg) if content == "" && role != "system" { t.Logf("⚠️ 消息[%d] (ID: %s, 角色: %s) 内容为空", i, msgID, role) } // 检查时间戳 timestamp, err := extractTimestamp(msg) if err != nil { t.Logf("⚠️ 消息[%d] (ID: %s) 无法提取时间戳: %v", i, msgID, err) } else { t.Logf(" 消息[%d] 时间戳: %v", i, timestamp.Format("2006-01-02 15:04:05")) } } }) // 2. 验证消息排序(时间倒序:最新在前) t.Run("MessageSortingOrder", func(t *testing.T) { if len(messages) < 2 { t.Skip("消息数量不足,跳过排序测试") } // 收集所有有效的时间戳 var timestamps []time.Time var validMessages []opencode.SessionMessage for i, msg := range messages { timestamp, err := extractTimestamp(msg) if err == nil { timestamps = append(timestamps, timestamp) validMessages = append(validMessages, msg) } else { t.Logf("⚠️ 消息[%d] 跳过排序检查(无有效时间戳)", i) } } if len(timestamps) < 2 { t.Skip("有效时间戳数量不足,跳过排序测试") } // 检查排序方向 // 先确定排序方向:检查前几对消息 var isDescending bool if len(timestamps) >= 2 { // 检查前几对确定方向 descendingCount := 0 ascendingCount := 0 for i := 1; i < minForMessagesTest(5, len(timestamps)); i++ { if timestamps[i].Before(timestamps[i-1]) { descendingCount++ // 时间递减(最新在前) } else if timestamps[i].After(timestamps[i-1]) { ascendingCount++ // 时间递增(最早在前) } } if descendingCount > ascendingCount { isDescending = true t.Logf("✅ 消息按时间倒序排列(最新在前)") } else if ascendingCount > descendingCount { isDescending = false t.Logf("✅ 消息按时间顺序排列(最早在前)") } else { t.Logf("⚠️ 无法确定排序方向,可能时间戳相同或乱序") isDescending = false } // 验证排序一致性 if isDescending { // 应该时间递减 for i := 1; i < len(timestamps); i++ { if timestamps[i].After(timestamps[i-1]) { t.Logf("❌ 倒序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更晚", i, timestamps[i].Format("15:04:05"), i-1, timestamps[i-1].Format("15:04:05")) } } } else { // 应该时间递增 for i := 1; i < len(timestamps); i++ { if timestamps[i].Before(timestamps[i-1]) { t.Logf("❌ 顺序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更早", i, timestamps[i].Format("15:04:05"), i-1, timestamps[i-1].Format("15:04:05")) } } } } if isDescending { t.Logf("✅ 消息按时间倒序排列(最新在前)") // 打印时间戳范围 if len(timestamps) > 0 { firstTime := timestamps[0] lastTime := timestamps[len(timestamps)-1] t.Logf("📊 时间戳范围:") t.Logf(" 第一条消息: %v (%v)", firstTime.Format("2006-01-02 15:04:05"), firstTime.Unix()) t.Logf(" 最后一条消息: %v (%v)", lastTime.Format("2006-01-02 15:04:05"), lastTime.Unix()) t.Logf(" 时间跨度: %v", lastTime.Sub(firstTime)) // 打印前3条和后3条 t.Logf(" 前3条消息时间:") for i := 0; i < minForMessagesTest(3, len(timestamps)); i++ { t.Logf(" [%d]: %v", i, timestamps[i].Format("15:04:05")) } if len(timestamps) > 6 { t.Logf(" 后3条消息时间:") for i := len(timestamps) - 3; i < len(timestamps); i++ { t.Logf(" [%d]: %v", i, timestamps[i].Format("15:04:05")) } } } } // 不因为排序方向而失败,只是记录信息 if !isDescending { t.Logf("ℹ️ 消息按时间顺序排列(最早在前),这对于增量加载是好事") } }) // 3. 验证limit参数对排序的影响 t.Run("LimitParameterEffect", func(t *testing.T) { // 获取带limit的消息 limit := 5 if len(messages) < limit { limit = len(messages) } limitedMessages, err := client.GetSessionMessages(ctx, sessionID, limit) if err != nil { t.Fatalf("❌ 获取限制数量的消息失败: %v", err) } t.Logf("✅ 获取到 %d 条限制消息", len(limitedMessages)) // 检查limit是否有效 if len(limitedMessages) > limit { t.Errorf("❌ limit参数无效: 返回 %d 条消息,限制为 %d", len(limitedMessages), limit) } // 检查limit消息是否与原消息前N条一致 if len(limitedMessages) > 0 && len(messages) >= len(limitedMessages) { for i := 0; i < len(limitedMessages); i++ { limitedID := extractMessageID(limitedMessages[i]) fullID := extractMessageID(messages[i]) if limitedID != fullID { t.Errorf("❌ limit消息不匹配: 位置[%d] limit消息ID=%s, 全量消息ID=%s", i, limitedID, fullID) break } } if t.Failed() { t.Logf("⚠️ limit消息与全量消息前N条不一致") } else { t.Logf("✅ limit参数正确返回前%d条消息", limit) } } }) // 4. 输出消息格式摘要 t.Run("MessageFormatSummary", func(t *testing.T) { t.Logf("📋 消息格式摘要:") t.Logf(" 消息总数: %d", len(messages)) // 角色统计 roleCount := make(map[string]int) for _, msg := range messages { role := extractMessageRole(msg) roleCount[role]++ } for role, count := range roleCount { t.Logf(" 角色 '%s': %d 条", role, count) } // 时间戳可用性统计 timestampCount := 0 for _, msg := range messages { _, err := extractTimestamp(msg) if err == nil { timestampCount++ } } t.Logf(" 有效时间戳: %d/%d (%.1f%%)", timestampCount, len(messages), float64(timestampCount)/float64(len(messages))*100) // 内容长度统计 totalContentLength := 0 for _, msg := range messages { content := extractMessageContent(msg) totalContentLength += len(content) } t.Logf(" 总内容长度: %d 字符", totalContentLength) if len(messages) > 0 { t.Logf(" 平均内容长度: %.1f 字符", float64(totalContentLength)/float64(len(messages))) } // 打印第一条消息的完整结构(用于调试) if len(messages) > 0 { t.Logf("🔍 第一条消息完整结构:") firstMsg := messages[0] if firstMsg.Info != nil { infoJSON, _ := json.MarshalIndent(firstMsg.Info, " ", " ") t.Logf(" Info: %s", infoJSON) // 打印Info的所有键 t.Logf(" Info keys: %v", getMapKeys(firstMsg.Info)) } else { t.Logf(" Info: nil") } if len(firstMsg.Parts) > 0 { partsJSON, _ := json.MarshalIndent(firstMsg.Parts, " ", " ") t.Logf(" Parts: %s", partsJSON) } else { t.Logf(" Parts: 空数组") } } }) } // TestMessageQueryPerformance 测试消息查询性能 func TestMessageQueryPerformance(t *testing.T) { // 外部已启动的OpenCode服务端口 externalOpenCodePort := 8787 opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort) // 检查OpenCode服务是否运行 if !isServiceRunningForMessages(t, opencodeURL) { t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL) } // 检查svc-code服务是否运行 svcCodeURL := "http://localhost:8020" if !isServiceRunningForMessages(t, svcCodeURL) { t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL) } t.Logf("🚀 开始测试消息查询性能") t.Logf("OpenCode URL: %s", opencodeURL) t.Logf("svc-code URL: %s", svcCodeURL) // 使用现有的测试会话ID sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z" t.Logf("测试会话ID: %s", sessionID) // 先获取总消息数,以确定合适的limit值 t.Log("📊 获取消息总数...") client, err := opencode.NewDirectClient(externalOpenCodePort) if err != nil { t.Fatalf("❌ 创建OpenCode客户端失败: %v", err) } ctx := context.Background() allMessages, err := client.GetSessionMessages(ctx, sessionID, 0) if err != nil { t.Fatalf("❌ 获取总消息失败: %v", err) } totalMessages := len(allMessages) t.Logf("📊 会话总消息数: %d", totalMessages) // 定义要测试的limit值 testLimits := []int{2, 5, 10, 20, 50, 100, 200, 300} // 如果总消息数小于某个limit,调整测试值 var adjustedLimits []int for _, limit := range testLimits { if limit <= totalMessages { adjustedLimits = append(adjustedLimits, limit) } } adjustedLimits = append(adjustedLimits, 0) // 0表示无限制(全量) t.Logf("📊 测试的limit值: %v", adjustedLimits) // 1. 测试直接OpenCode API性能 t.Run("DirectOpenCodeAPI", func(t *testing.T) { t.Log("📊 测试直接OpenCode API性能...") for _, limit := range adjustedLimits { start := time.Now() // 构造URL url := fmt.Sprintf("%s/session/%s/message", opencodeURL, sessionID) if limit > 0 { url = fmt.Sprintf("%s?limit=%d", url, limit) } // 发送请求 client := &http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest("GET", url, nil) if err != nil { t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err) continue } req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err) continue } // 读取响应体(确保完全读取以测量网络传输时间) body, err := io.ReadAll(resp.Body) resp.Body.Close() duration := time.Since(start) if err != nil { t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err) continue } if resp.StatusCode != http.StatusOK { t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d", limit, resp.StatusCode) continue } // 解析消息数量(可选) var messages []opencode.SessionMessage if err := json.Unmarshal(body, &messages); err != nil { t.Logf("⚠️ 解析消息失败 (limit=%d): %v", limit, err) } actualCount := len(messages) t.Logf(" limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节", limit, duration, actualCount, len(body)) } }) // 2. 测试通过svc-code API性能 t.Run("SvcCodeAPI", func(t *testing.T) { t.Log("📊 测试svc-code API性能...") // 用户登录获取token token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL) if err != nil { t.Fatalf("❌ 登录失败: %v", err) } for _, limit := range adjustedLimits { start := time.Now() // 构造URL url := fmt.Sprintf("%s/api/session/messages", svcCodeURL) // 构建请求体(POST方法) requestBody := map[string]interface{}{ "sessionID": sessionID, } if limit > 0 { requestBody["limit"] = limit } jsonBody, err := json.Marshal(requestBody) if err != nil { t.Errorf("❌ 编码请求体失败 (limit=%d): %v", limit, err) continue } // 创建POST请求 req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) if err != nil { t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err) continue } // 添加认证头和内容类型 req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") // 发送请求 client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err) continue } // 读取响应体 body, err := io.ReadAll(resp.Body) resp.Body.Close() duration := time.Since(start) if err != nil { t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err) continue } if resp.StatusCode != http.StatusOK { t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d, 响应体: %s", limit, resp.StatusCode, string(body[:minForMessagesTest(200, len(body))])) continue } // 解析响应 var result struct { Success bool `json:"success"` Message string `json:"message"` Data struct { Messages []opencode.SessionMessage `json:"messages"` Count int `json:"count"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { t.Errorf("❌ 解析响应失败 (limit=%d): %v, 响应体: %s", limit, err, string(body[:minForMessagesTest(200, len(body))])) continue } if !result.Success { t.Errorf("❌ API调用失败 (limit=%d): %s", limit, result.Message) continue } actualCount := len(result.Data.Messages) t.Logf(" limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节", limit, duration, actualCount, len(body)) } }) // 3. 性能对比总结 t.Run("PerformanceSummary", func(t *testing.T) { t.Log("📊 性能对比总结:") t.Log(" (具体数据见上述测试输出)") t.Log(" 📈 预期趋势:") t.Log(" 1. limit越小,响应时间越短") t.Log(" 2. 响应体大小与消息数量成正比") t.Log(" 3. svc-code API会有额外开销(认证、封装)") t.Log(" 💡 优化建议:") t.Log(" 1. 增量加载:只查询新消息(limit=新消息数量)") t.Log(" 2. 缓存:避免重复查询相同消息") t.Log(" 3. 前端过滤:即使查询全量,也可缓存过滤") }) }