Aucune description
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

session_messages_test.go 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. package main
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "testing"
  10. "time"
  11. "git.x2erp.com/qdy/go-svc-code/internal/opencode"
  12. )
  13. // TestDirectOpenCodeMessages 直接调用opencode API (8787端口) 获取会话消息
  14. func TestDirectOpenCodeMessages(t *testing.T) {
  15. // 外部已启动的OpenCode服务端口
  16. externalOpenCodePort := 8787
  17. opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
  18. // 检查OpenCode服务是否运行
  19. if !isServiceRunningForMessages(t, opencodeURL) {
  20. t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
  21. }
  22. t.Logf("🚀 开始测试直接调用OpenCode API获取消息")
  23. t.Logf("OpenCode URL: %s", opencodeURL)
  24. // 使用现有的测试会话ID
  25. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  26. t.Logf("测试会话ID: %s", sessionID)
  27. // 1. 使用DirectClient调用(测试SDK接口)
  28. t.Run("SDKClient", func(t *testing.T) {
  29. testDirectClientMessages(t, externalOpenCodePort, sessionID)
  30. })
  31. // 2. 直接HTTP调用(测试原始API)
  32. t.Run("RawHTTP", func(t *testing.T) {
  33. testRawHTTPMessages(t, opencodeURL, sessionID)
  34. })
  35. }
  36. // testDirectClientMessages 使用DirectClient测试获取消息
  37. func testDirectClientMessages(t *testing.T, port int, sessionID string) {
  38. // 创建客户端
  39. client, err := opencode.NewDirectClient(port)
  40. if err != nil {
  41. t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
  42. }
  43. t.Logf("✅ 创建OpenCode客户端成功,基础URL: %s", client.GetBaseURL())
  44. ctx := context.Background()
  45. // 测试不带limit参数
  46. t.Log("📝 测试获取消息(不带limit)")
  47. messages, err := client.GetSessionMessages(ctx, sessionID, 0)
  48. if err != nil {
  49. t.Errorf("❌ 获取会话消息失败: %v", err)
  50. return
  51. }
  52. t.Logf("✅ 获取到 %d 条消息", len(messages))
  53. // 打印消息摘要
  54. for i, msg := range messages {
  55. t.Logf(" 消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg))
  56. if i < 3 && len(messages) > 3 { // 只打印前3条
  57. if content := extractMessageContent(msg); content != "" {
  58. t.Logf(" 内容预览: %.100s...", content)
  59. }
  60. }
  61. }
  62. // 测试带limit参数
  63. t.Log("📝 测试获取消息(limit=5)")
  64. messagesLimited, err := client.GetSessionMessages(ctx, sessionID, 5)
  65. if err != nil {
  66. t.Errorf("❌ 获取限制数量的会话消息失败: %v", err)
  67. return
  68. }
  69. t.Logf("✅ 获取到 %d 条消息(限制5条)", len(messagesLimited))
  70. if len(messages) > 0 && len(messagesLimited) > 0 {
  71. if len(messagesLimited) > 5 {
  72. t.Errorf("❌ 消息数量超过限制: %d > 5", len(messagesLimited))
  73. }
  74. }
  75. }
  76. // testRawHTTPMessages 直接HTTP调用测试获取消息
  77. func testRawHTTPMessages(t *testing.T, baseURL, sessionID string) {
  78. // 构造URL
  79. url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID)
  80. t.Logf("📡 调用原始API: %s", url)
  81. // 发送HTTP请求
  82. client := &http.Client{Timeout: 10 * time.Second}
  83. req, err := http.NewRequest("GET", url, nil)
  84. if err != nil {
  85. t.Fatalf("❌ 创建请求失败: %v", err)
  86. }
  87. req.Header.Set("Accept", "application/json")
  88. resp, err := client.Do(req)
  89. if err != nil {
  90. t.Fatalf("❌ HTTP请求失败: %v", err)
  91. }
  92. defer resp.Body.Close()
  93. t.Logf("📊 响应状态码: %d", resp.StatusCode)
  94. if resp.StatusCode != http.StatusOK {
  95. body, _ := io.ReadAll(resp.Body)
  96. t.Fatalf("❌ 获取会话消息失败,状态码: %d, 响应体: %s", resp.StatusCode, string(body))
  97. }
  98. // 解析响应
  99. body, err := io.ReadAll(resp.Body)
  100. if err != nil {
  101. t.Fatalf("❌ 读取响应体失败: %v", err)
  102. }
  103. // 尝试解析为SessionMessage数组
  104. var messages []opencode.SessionMessage
  105. if err := json.Unmarshal(body, &messages); err != nil {
  106. t.Errorf("❌ 解析消息响应失败: %v", err)
  107. t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))]))
  108. return
  109. }
  110. t.Logf("✅ 解析成功,获取到 %d 条消息", len(messages))
  111. // 验证消息结构
  112. for i, msg := range messages {
  113. if extractMessageID(msg) == "" {
  114. t.Errorf("❌ 消息[%d]缺少ID", i)
  115. }
  116. if extractMessageRole(msg) == "" {
  117. t.Errorf("❌ 消息[%d]缺少角色", i)
  118. }
  119. }
  120. // 测试带limit参数
  121. urlWithLimit := fmt.Sprintf("%s?limit=3", url)
  122. t.Logf("📡 调用带limit的API: %s", urlWithLimit)
  123. reqLimit, err := http.NewRequest("GET", urlWithLimit, nil)
  124. if err != nil {
  125. t.Errorf("❌ 创建带limit的请求失败: %v", err)
  126. return
  127. }
  128. reqLimit.Header.Set("Accept", "application/json")
  129. respLimit, err := client.Do(reqLimit)
  130. if err != nil {
  131. t.Errorf("❌ HTTP请求(带limit)失败: %v", err)
  132. return
  133. }
  134. defer respLimit.Body.Close()
  135. if respLimit.StatusCode == http.StatusOK {
  136. bodyLimit, _ := io.ReadAll(respLimit.Body)
  137. var messagesLimit []opencode.SessionMessage
  138. if err := json.Unmarshal(bodyLimit, &messagesLimit); err == nil {
  139. t.Logf("✅ 带limit获取到 %d 条消息", len(messagesLimit))
  140. if len(messagesLimit) > 3 {
  141. t.Errorf("❌ 带limit的消息数量超过限制: %d > 3", len(messagesLimit))
  142. }
  143. }
  144. }
  145. }
  146. // TestSvcCodeMessagesAPI 通过svc-code API (8020端口) 获取会话消息
  147. func TestSvcCodeMessagesAPI(t *testing.T) {
  148. // svc-code服务地址
  149. svcCodeURL := "http://localhost:8020"
  150. // 检查svc-code服务是否运行
  151. if !isServiceRunningForMessages(t, svcCodeURL) {
  152. t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
  153. }
  154. t.Logf("🚀 开始测试通过svc-code API获取消息")
  155. t.Logf("svc-code URL: %s", svcCodeURL)
  156. // 使用现有的测试会话ID
  157. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  158. t.Logf("测试会话ID: %s", sessionID)
  159. // 1. 用户登录获取token
  160. token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL)
  161. if err != nil {
  162. t.Fatalf("❌ 登录失败: %v", err)
  163. }
  164. t.Logf("✅ 获取到Token: %s...", token[:minForMessagesTest(20, len(token))])
  165. // 2. 测试不带limit参数
  166. t.Run("WithoutLimit", func(t *testing.T) {
  167. testSvcCodeMessages(t, svcCodeURL, token, sessionID, 0)
  168. })
  169. // 3. 测试带limit参数
  170. t.Run("WithLimit", func(t *testing.T) {
  171. testSvcCodeMessages(t, svcCodeURL, token, sessionID, 10)
  172. })
  173. // 4. 测试无效token
  174. t.Run("InvalidToken", func(t *testing.T) {
  175. testInvalidTokenForMessages(t, svcCodeURL, sessionID)
  176. })
  177. // 5. 测试无效sessionID
  178. t.Run("InvalidSessionID", func(t *testing.T) {
  179. testInvalidSessionIDForMessages(t, svcCodeURL, token)
  180. })
  181. }
  182. // testSvcCodeMessages 测试svc-code获取消息API
  183. func testSvcCodeMessages(t *testing.T, baseURL, token, sessionID string, limit int) {
  184. // 构造URL
  185. url := fmt.Sprintf("%s/api/session/messages", baseURL)
  186. // 创建请求
  187. req, err := http.NewRequest("GET", url, nil)
  188. if err != nil {
  189. t.Fatalf("❌ 创建请求失败: %v", err)
  190. }
  191. // 添加查询参数
  192. q := req.URL.Query()
  193. q.Add("sessionID", sessionID)
  194. if limit > 0 {
  195. q.Add("limit", fmt.Sprintf("%d", limit))
  196. }
  197. req.URL.RawQuery = q.Encode()
  198. // 添加认证头
  199. req.Header.Set("Authorization", "Bearer "+token)
  200. req.Header.Set("Accept", "application/json")
  201. t.Logf("📡 调用svc-code API: %s", req.URL.String())
  202. // 发送请求
  203. client := &http.Client{Timeout: 10 * time.Second}
  204. resp, err := client.Do(req)
  205. if err != nil {
  206. t.Fatalf("❌ HTTP请求失败: %v", err)
  207. }
  208. defer resp.Body.Close()
  209. t.Logf("📊 响应状态码: %d", resp.StatusCode)
  210. body, err := io.ReadAll(resp.Body)
  211. if err != nil {
  212. t.Fatalf("❌ 读取响应体失败: %v", err)
  213. }
  214. // 解析响应
  215. var result struct {
  216. Success bool `json:"success"`
  217. Message string `json:"message"`
  218. Data struct {
  219. Messages []opencode.SessionMessage `json:"messages"`
  220. Count int `json:"count"`
  221. } `json:"data"`
  222. }
  223. if err := json.Unmarshal(body, &result); err != nil {
  224. t.Errorf("❌ 解析响应失败: %v", err)
  225. t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))]))
  226. return
  227. }
  228. if !result.Success {
  229. t.Errorf("❌ API调用失败: %s", result.Message)
  230. return
  231. }
  232. t.Logf("✅ 获取成功!消息数量: %d, 总计: %d",
  233. len(result.Data.Messages), result.Data.Count)
  234. // 验证数据一致性
  235. if result.Data.Count != len(result.Data.Messages) {
  236. t.Errorf("❌ 数据不一致: Count=%d, Messages长度=%d",
  237. result.Data.Count, len(result.Data.Messages))
  238. }
  239. // 打印消息摘要
  240. for i, msg := range result.Data.Messages {
  241. t.Logf(" 消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg))
  242. if i < 2 && len(result.Data.Messages) > 2 {
  243. if content := extractMessageContent(msg); content != "" {
  244. t.Logf(" 内容预览: %.100s...", content)
  245. }
  246. }
  247. }
  248. // 验证limit参数效果
  249. if limit > 0 && len(result.Data.Messages) > limit {
  250. t.Errorf("❌ 消息数量超过限制: %d > %d", len(result.Data.Messages), limit)
  251. }
  252. }
  253. // testInvalidTokenForMessages 测试无效token
  254. func testInvalidTokenForMessages(t *testing.T, baseURL, sessionID string) {
  255. url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, sessionID)
  256. req, err := http.NewRequest("GET", url, nil)
  257. if err != nil {
  258. t.Fatalf("创建请求失败: %v", err)
  259. }
  260. // 使用无效token
  261. req.Header.Set("Authorization", "Bearer invalid_token_12345")
  262. client := &http.Client{Timeout: 5 * time.Second}
  263. resp, err := client.Do(req)
  264. if err != nil {
  265. t.Fatalf("HTTP请求失败: %v", err)
  266. }
  267. defer resp.Body.Close()
  268. // 应该返回401或403
  269. if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
  270. t.Errorf("预期认证错误状态码(401/403),实际: %d", resp.StatusCode)
  271. body, _ := io.ReadAll(resp.Body)
  272. t.Logf("响应体: %s", string(body))
  273. } else {
  274. t.Logf("✅ 无效token测试通过,返回状态码: %d", resp.StatusCode)
  275. }
  276. }
  277. // testInvalidSessionIDForMessages 测试无效sessionID
  278. func testInvalidSessionIDForMessages(t *testing.T, baseURL, token string) {
  279. invalidSessionID := "ses_invalid_12345"
  280. url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, invalidSessionID)
  281. req, err := http.NewRequest("GET", url, nil)
  282. if err != nil {
  283. t.Fatalf("创建请求失败: %v", err)
  284. }
  285. req.Header.Set("Authorization", "Bearer "+token)
  286. client := &http.Client{Timeout: 5 * time.Second}
  287. resp, err := client.Do(req)
  288. if err != nil {
  289. t.Fatalf("HTTP请求失败: %v", err)
  290. }
  291. defer resp.Body.Close()
  292. body, err := io.ReadAll(resp.Body)
  293. if err != nil {
  294. t.Fatalf("读取响应体失败: %v", err)
  295. }
  296. var result struct {
  297. Success bool `json:"success"`
  298. Message string `json:"message"`
  299. }
  300. if err := json.Unmarshal(body, &result); err != nil {
  301. t.Errorf("解析响应失败: %v", err)
  302. return
  303. }
  304. if result.Success {
  305. t.Errorf("无效sessionID应该返回失败,实际成功")
  306. } else {
  307. t.Logf("✅ 无效sessionID测试通过,错误信息: %s", result.Message)
  308. }
  309. }
  310. // loginAndGetTokenForMessagesTest 登录获取token
  311. func loginAndGetTokenForMessagesTest(t *testing.T, baseURL string) (string, error) {
  312. loginURL := baseURL + "/api/auth/login"
  313. loginData := map[string]string{
  314. "user_id": "test-user-001",
  315. "password": "password123",
  316. }
  317. jsonData, _ := json.Marshal(loginData)
  318. resp, err := http.Post(loginURL, "application/json", bytes.NewBuffer(jsonData))
  319. if err != nil {
  320. return "", fmt.Errorf("登录请求失败: %v", err)
  321. }
  322. defer resp.Body.Close()
  323. if resp.StatusCode != http.StatusOK {
  324. bodyBytes, _ := io.ReadAll(resp.Body)
  325. return "", fmt.Errorf("登录失败 (状态码 %d): %s", resp.StatusCode, string(bodyBytes))
  326. }
  327. var result struct {
  328. Success bool `json:"success"`
  329. Data string `json:"data"`
  330. Message string `json:"message"`
  331. }
  332. bodyBytes, _ := io.ReadAll(resp.Body)
  333. if err := json.Unmarshal(bodyBytes, &result); err != nil {
  334. return "", fmt.Errorf("解析响应失败: %v", err)
  335. }
  336. if !result.Success {
  337. return "", fmt.Errorf("登录失败: %s", result.Message)
  338. }
  339. return result.Data, nil
  340. }
  341. // isServiceRunningForMessages 检查服务是否运行
  342. func isServiceRunningForMessages(t *testing.T, url string) bool {
  343. client := &http.Client{Timeout: 3 * time.Second}
  344. // 尝试多个端点
  345. endpoints := []string{
  346. "/global/health", // opencode健康检查
  347. "/api/auth/health", // svc-code健康检查
  348. "/api/health", // 其他健康检查
  349. "", // 根路径
  350. }
  351. for _, endpoint := range endpoints {
  352. fullURL := url + endpoint
  353. resp, err := client.Get(fullURL)
  354. if err == nil {
  355. resp.Body.Close()
  356. if resp.StatusCode == http.StatusOK {
  357. t.Logf("✅ 服务运行正常: %s (状态码: %d)", fullURL, resp.StatusCode)
  358. return true
  359. }
  360. }
  361. }
  362. return false
  363. }
  364. // extractMessageID 从SessionMessage提取消息ID
  365. func extractMessageID(msg opencode.SessionMessage) string {
  366. if msg.Info != nil {
  367. if id, ok := msg.Info["id"].(string); ok {
  368. return id
  369. }
  370. }
  371. return ""
  372. }
  373. // extractMessageRole 从SessionMessage提取消息角色
  374. func extractMessageRole(msg opencode.SessionMessage) string {
  375. if msg.Info != nil {
  376. if role, ok := msg.Info["role"].(string); ok {
  377. return role
  378. }
  379. }
  380. return ""
  381. }
  382. // extractMessageContent 从SessionMessage提取消息内容
  383. func extractMessageContent(msg opencode.SessionMessage) string {
  384. if len(msg.Parts) > 0 {
  385. for _, part := range msg.Parts {
  386. if text, ok := part["text"].(string); ok && text != "" {
  387. return text
  388. }
  389. }
  390. }
  391. return ""
  392. }
  393. // minForMessagesTest 返回两个整数的最小值
  394. func minForMessagesTest(a, b int) int {
  395. if a < b {
  396. return a
  397. }
  398. return b
  399. }
  400. // extractTimestamp 从SessionMessage提取时间戳
  401. func extractTimestamp(msg opencode.SessionMessage) (time.Time, error) {
  402. // 尝试从info.time.created提取
  403. if msg.Info != nil {
  404. // 调试:打印info的所有键
  405. // fmt.Printf("Info keys: %v\n", getMapKeys(msg.Info))
  406. // 1. 尝试从time.created提取(毫秒时间戳)
  407. if timeMap, ok := msg.Info["time"].(map[string]interface{}); ok {
  408. // fmt.Printf("Time map keys: %v\n", getMapKeys(timeMap))
  409. // 尝试毫秒时间戳(整数)
  410. if createdMs, ok := timeMap["created"].(float64); ok {
  411. // 转换为秒(除以1000)
  412. seconds := int64(createdMs) / 1000
  413. nanoseconds := (int64(createdMs) % 1000) * 1000000
  414. return time.Unix(seconds, nanoseconds), nil
  415. }
  416. // 尝试字符串格式
  417. if createdStr, ok := timeMap["created"].(string); ok {
  418. return time.Parse(time.RFC3339, createdStr)
  419. }
  420. }
  421. // 2. 尝试直接提取created(毫秒时间戳)
  422. if createdMs, ok := msg.Info["created"].(float64); ok {
  423. seconds := int64(createdMs) / 1000
  424. nanoseconds := (int64(createdMs) % 1000) * 1000000
  425. return time.Unix(seconds, nanoseconds), nil
  426. }
  427. // 3. 尝试从created_at提取(字符串或数字)
  428. if createdVal, ok := msg.Info["created_at"]; ok {
  429. switch v := createdVal.(type) {
  430. case float64:
  431. seconds := int64(v) / 1000
  432. nanoseconds := (int64(v) % 1000) * 1000000
  433. return time.Unix(seconds, nanoseconds), nil
  434. case string:
  435. return time.Parse(time.RFC3339, v)
  436. }
  437. }
  438. // 4. 尝试从createdAt提取(驼峰式)
  439. if createdAtVal, ok := msg.Info["createdAt"]; ok {
  440. switch v := createdAtVal.(type) {
  441. case float64:
  442. seconds := int64(v) / 1000
  443. nanoseconds := (int64(v) % 1000) * 1000000
  444. return time.Unix(seconds, nanoseconds), nil
  445. case string:
  446. return time.Parse(time.RFC3339, v)
  447. }
  448. }
  449. // 5. 尝试从timestamp提取
  450. if timestampFloat, ok := msg.Info["timestamp"].(float64); ok {
  451. return time.Unix(int64(timestampFloat), 0), nil
  452. }
  453. // 6. 尝试从date提取
  454. if dateStr, ok := msg.Info["date"].(string); ok {
  455. return time.Parse(time.RFC3339, dateStr)
  456. }
  457. // 7. 尝试从time直接提取(可能是字符串)
  458. if timeStr, ok := msg.Info["time"].(string); ok {
  459. return time.Parse(time.RFC3339, timeStr)
  460. }
  461. }
  462. return time.Time{}, fmt.Errorf("无法提取时间戳")
  463. }
  464. // getMapKeys 获取map的所有键(用于调试)
  465. func getMapKeys(m map[string]interface{}) []string {
  466. keys := make([]string, 0, len(m))
  467. for k := range m {
  468. keys = append(keys, k)
  469. }
  470. return keys
  471. }
  472. // TestMessageFormatAndSorting 测试消息格式和排序规则
  473. func TestMessageFormatAndSorting(t *testing.T) {
  474. // 外部已启动的OpenCode服务端口
  475. externalOpenCodePort := 8787
  476. opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
  477. // 检查OpenCode服务是否运行
  478. if !isServiceRunningForMessages(t, opencodeURL) {
  479. t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
  480. }
  481. t.Logf("🚀 开始测试消息格式和排序规则")
  482. t.Logf("OpenCode URL: %s", opencodeURL)
  483. // 使用现有的测试会话ID
  484. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  485. t.Logf("测试会话ID: %s", sessionID)
  486. // 创建客户端
  487. client, err := opencode.NewDirectClient(externalOpenCodePort)
  488. if err != nil {
  489. t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
  490. }
  491. ctx := context.Background()
  492. // 获取消息(不带limit)
  493. t.Log("📝 获取消息进行格式和排序分析")
  494. messages, err := client.GetSessionMessages(ctx, sessionID, 0)
  495. if err != nil {
  496. t.Fatalf("❌ 获取会话消息失败: %v", err)
  497. }
  498. t.Logf("✅ 获取到 %d 条消息进行分析", len(messages))
  499. if len(messages) == 0 {
  500. t.Skip("会话中没有消息,跳过格式和排序测试")
  501. }
  502. // 1. 验证消息格式完整性
  503. t.Run("MessageFormatIntegrity", func(t *testing.T) {
  504. for i, msg := range messages {
  505. // 检查消息ID
  506. msgID := extractMessageID(msg)
  507. if msgID == "" {
  508. t.Errorf("❌ 消息[%d]缺少ID", i)
  509. }
  510. // 检查角色
  511. role := extractMessageRole(msg)
  512. if role == "" {
  513. t.Errorf("❌ 消息[%d]缺少角色", i)
  514. }
  515. // 检查内容
  516. content := extractMessageContent(msg)
  517. if content == "" && role != "system" {
  518. t.Logf("⚠️ 消息[%d] (ID: %s, 角色: %s) 内容为空", i, msgID, role)
  519. }
  520. // 检查时间戳
  521. timestamp, err := extractTimestamp(msg)
  522. if err != nil {
  523. t.Logf("⚠️ 消息[%d] (ID: %s) 无法提取时间戳: %v", i, msgID, err)
  524. } else {
  525. t.Logf(" 消息[%d] 时间戳: %v", i, timestamp.Format("2006-01-02 15:04:05"))
  526. }
  527. }
  528. })
  529. // 2. 验证消息排序(时间倒序:最新在前)
  530. t.Run("MessageSortingOrder", func(t *testing.T) {
  531. if len(messages) < 2 {
  532. t.Skip("消息数量不足,跳过排序测试")
  533. }
  534. // 收集所有有效的时间戳
  535. var timestamps []time.Time
  536. var validMessages []opencode.SessionMessage
  537. for i, msg := range messages {
  538. timestamp, err := extractTimestamp(msg)
  539. if err == nil {
  540. timestamps = append(timestamps, timestamp)
  541. validMessages = append(validMessages, msg)
  542. } else {
  543. t.Logf("⚠️ 消息[%d] 跳过排序检查(无有效时间戳)", i)
  544. }
  545. }
  546. if len(timestamps) < 2 {
  547. t.Skip("有效时间戳数量不足,跳过排序测试")
  548. }
  549. // 检查排序方向
  550. // 先确定排序方向:检查前几对消息
  551. var isDescending bool
  552. if len(timestamps) >= 2 {
  553. // 检查前几对确定方向
  554. descendingCount := 0
  555. ascendingCount := 0
  556. for i := 1; i < minForMessagesTest(5, len(timestamps)); i++ {
  557. if timestamps[i].Before(timestamps[i-1]) {
  558. descendingCount++ // 时间递减(最新在前)
  559. } else if timestamps[i].After(timestamps[i-1]) {
  560. ascendingCount++ // 时间递增(最早在前)
  561. }
  562. }
  563. if descendingCount > ascendingCount {
  564. isDescending = true
  565. t.Logf("✅ 消息按时间倒序排列(最新在前)")
  566. } else if ascendingCount > descendingCount {
  567. isDescending = false
  568. t.Logf("✅ 消息按时间顺序排列(最早在前)")
  569. } else {
  570. t.Logf("⚠️ 无法确定排序方向,可能时间戳相同或乱序")
  571. isDescending = false
  572. }
  573. // 验证排序一致性
  574. if isDescending {
  575. // 应该时间递减
  576. for i := 1; i < len(timestamps); i++ {
  577. if timestamps[i].After(timestamps[i-1]) {
  578. t.Logf("❌ 倒序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更晚",
  579. i, timestamps[i].Format("15:04:05"),
  580. i-1, timestamps[i-1].Format("15:04:05"))
  581. }
  582. }
  583. } else {
  584. // 应该时间递增
  585. for i := 1; i < len(timestamps); i++ {
  586. if timestamps[i].Before(timestamps[i-1]) {
  587. t.Logf("❌ 顺序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更早",
  588. i, timestamps[i].Format("15:04:05"),
  589. i-1, timestamps[i-1].Format("15:04:05"))
  590. }
  591. }
  592. }
  593. }
  594. if isDescending {
  595. t.Logf("✅ 消息按时间倒序排列(最新在前)")
  596. // 打印时间戳范围
  597. if len(timestamps) > 0 {
  598. firstTime := timestamps[0]
  599. lastTime := timestamps[len(timestamps)-1]
  600. t.Logf("📊 时间戳范围:")
  601. t.Logf(" 第一条消息: %v (%v)", firstTime.Format("2006-01-02 15:04:05"), firstTime.Unix())
  602. t.Logf(" 最后一条消息: %v (%v)", lastTime.Format("2006-01-02 15:04:05"), lastTime.Unix())
  603. t.Logf(" 时间跨度: %v", lastTime.Sub(firstTime))
  604. // 打印前3条和后3条
  605. t.Logf(" 前3条消息时间:")
  606. for i := 0; i < minForMessagesTest(3, len(timestamps)); i++ {
  607. t.Logf(" [%d]: %v", i, timestamps[i].Format("15:04:05"))
  608. }
  609. if len(timestamps) > 6 {
  610. t.Logf(" 后3条消息时间:")
  611. for i := len(timestamps) - 3; i < len(timestamps); i++ {
  612. t.Logf(" [%d]: %v", i, timestamps[i].Format("15:04:05"))
  613. }
  614. }
  615. }
  616. }
  617. // 不因为排序方向而失败,只是记录信息
  618. if !isDescending {
  619. t.Logf("ℹ️ 消息按时间顺序排列(最早在前),这对于增量加载是好事")
  620. }
  621. })
  622. // 3. 验证limit参数对排序的影响
  623. t.Run("LimitParameterEffect", func(t *testing.T) {
  624. // 获取带limit的消息
  625. limit := 5
  626. if len(messages) < limit {
  627. limit = len(messages)
  628. }
  629. limitedMessages, err := client.GetSessionMessages(ctx, sessionID, limit)
  630. if err != nil {
  631. t.Fatalf("❌ 获取限制数量的消息失败: %v", err)
  632. }
  633. t.Logf("✅ 获取到 %d 条限制消息", len(limitedMessages))
  634. // 检查limit是否有效
  635. if len(limitedMessages) > limit {
  636. t.Errorf("❌ limit参数无效: 返回 %d 条消息,限制为 %d", len(limitedMessages), limit)
  637. }
  638. // 检查limit消息是否与原消息前N条一致
  639. if len(limitedMessages) > 0 && len(messages) >= len(limitedMessages) {
  640. for i := 0; i < len(limitedMessages); i++ {
  641. limitedID := extractMessageID(limitedMessages[i])
  642. fullID := extractMessageID(messages[i])
  643. if limitedID != fullID {
  644. t.Errorf("❌ limit消息不匹配: 位置[%d] limit消息ID=%s, 全量消息ID=%s",
  645. i, limitedID, fullID)
  646. break
  647. }
  648. }
  649. if t.Failed() {
  650. t.Logf("⚠️ limit消息与全量消息前N条不一致")
  651. } else {
  652. t.Logf("✅ limit参数正确返回前%d条消息", limit)
  653. }
  654. }
  655. })
  656. // 4. 输出消息格式摘要
  657. t.Run("MessageFormatSummary", func(t *testing.T) {
  658. t.Logf("📋 消息格式摘要:")
  659. t.Logf(" 消息总数: %d", len(messages))
  660. // 角色统计
  661. roleCount := make(map[string]int)
  662. for _, msg := range messages {
  663. role := extractMessageRole(msg)
  664. roleCount[role]++
  665. }
  666. for role, count := range roleCount {
  667. t.Logf(" 角色 '%s': %d 条", role, count)
  668. }
  669. // 时间戳可用性统计
  670. timestampCount := 0
  671. for _, msg := range messages {
  672. _, err := extractTimestamp(msg)
  673. if err == nil {
  674. timestampCount++
  675. }
  676. }
  677. t.Logf(" 有效时间戳: %d/%d (%.1f%%)", timestampCount, len(messages),
  678. float64(timestampCount)/float64(len(messages))*100)
  679. // 内容长度统计
  680. totalContentLength := 0
  681. for _, msg := range messages {
  682. content := extractMessageContent(msg)
  683. totalContentLength += len(content)
  684. }
  685. t.Logf(" 总内容长度: %d 字符", totalContentLength)
  686. if len(messages) > 0 {
  687. t.Logf(" 平均内容长度: %.1f 字符", float64(totalContentLength)/float64(len(messages)))
  688. }
  689. // 打印第一条消息的完整结构(用于调试)
  690. if len(messages) > 0 {
  691. t.Logf("🔍 第一条消息完整结构:")
  692. firstMsg := messages[0]
  693. if firstMsg.Info != nil {
  694. infoJSON, _ := json.MarshalIndent(firstMsg.Info, " ", " ")
  695. t.Logf(" Info: %s", infoJSON)
  696. // 打印Info的所有键
  697. t.Logf(" Info keys: %v", getMapKeys(firstMsg.Info))
  698. } else {
  699. t.Logf(" Info: nil")
  700. }
  701. if len(firstMsg.Parts) > 0 {
  702. partsJSON, _ := json.MarshalIndent(firstMsg.Parts, " ", " ")
  703. t.Logf(" Parts: %s", partsJSON)
  704. } else {
  705. t.Logf(" Parts: 空数组")
  706. }
  707. }
  708. })
  709. }
  710. // TestMessageQueryPerformance 测试消息查询性能
  711. func TestMessageQueryPerformance(t *testing.T) {
  712. // 外部已启动的OpenCode服务端口
  713. externalOpenCodePort := 8787
  714. opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
  715. // 检查OpenCode服务是否运行
  716. if !isServiceRunningForMessages(t, opencodeURL) {
  717. t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
  718. }
  719. // 检查svc-code服务是否运行
  720. svcCodeURL := "http://localhost:8020"
  721. if !isServiceRunningForMessages(t, svcCodeURL) {
  722. t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
  723. }
  724. t.Logf("🚀 开始测试消息查询性能")
  725. t.Logf("OpenCode URL: %s", opencodeURL)
  726. t.Logf("svc-code URL: %s", svcCodeURL)
  727. // 使用现有的测试会话ID
  728. sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
  729. t.Logf("测试会话ID: %s", sessionID)
  730. // 先获取总消息数,以确定合适的limit值
  731. t.Log("📊 获取消息总数...")
  732. client, err := opencode.NewDirectClient(externalOpenCodePort)
  733. if err != nil {
  734. t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
  735. }
  736. ctx := context.Background()
  737. allMessages, err := client.GetSessionMessages(ctx, sessionID, 0)
  738. if err != nil {
  739. t.Fatalf("❌ 获取总消息失败: %v", err)
  740. }
  741. totalMessages := len(allMessages)
  742. t.Logf("📊 会话总消息数: %d", totalMessages)
  743. // 定义要测试的limit值
  744. testLimits := []int{2, 5, 10, 20, 50, 100, 200, 300}
  745. // 如果总消息数小于某个limit,调整测试值
  746. var adjustedLimits []int
  747. for _, limit := range testLimits {
  748. if limit <= totalMessages {
  749. adjustedLimits = append(adjustedLimits, limit)
  750. }
  751. }
  752. adjustedLimits = append(adjustedLimits, 0) // 0表示无限制(全量)
  753. t.Logf("📊 测试的limit值: %v", adjustedLimits)
  754. // 1. 测试直接OpenCode API性能
  755. t.Run("DirectOpenCodeAPI", func(t *testing.T) {
  756. t.Log("📊 测试直接OpenCode API性能...")
  757. for _, limit := range adjustedLimits {
  758. start := time.Now()
  759. // 构造URL
  760. url := fmt.Sprintf("%s/session/%s/message", opencodeURL, sessionID)
  761. if limit > 0 {
  762. url = fmt.Sprintf("%s?limit=%d", url, limit)
  763. }
  764. // 发送请求
  765. client := &http.Client{Timeout: 30 * time.Second}
  766. req, err := http.NewRequest("GET", url, nil)
  767. if err != nil {
  768. t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err)
  769. continue
  770. }
  771. req.Header.Set("Accept", "application/json")
  772. resp, err := client.Do(req)
  773. if err != nil {
  774. t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err)
  775. continue
  776. }
  777. // 读取响应体(确保完全读取以测量网络传输时间)
  778. body, err := io.ReadAll(resp.Body)
  779. resp.Body.Close()
  780. duration := time.Since(start)
  781. if err != nil {
  782. t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err)
  783. continue
  784. }
  785. if resp.StatusCode != http.StatusOK {
  786. t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d", limit, resp.StatusCode)
  787. continue
  788. }
  789. // 解析消息数量(可选)
  790. var messages []opencode.SessionMessage
  791. if err := json.Unmarshal(body, &messages); err != nil {
  792. t.Logf("⚠️ 解析消息失败 (limit=%d): %v", limit, err)
  793. }
  794. actualCount := len(messages)
  795. t.Logf(" limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节",
  796. limit, duration, actualCount, len(body))
  797. }
  798. })
  799. // 2. 测试通过svc-code API性能
  800. t.Run("SvcCodeAPI", func(t *testing.T) {
  801. t.Log("📊 测试svc-code API性能...")
  802. // 用户登录获取token
  803. token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL)
  804. if err != nil {
  805. t.Fatalf("❌ 登录失败: %v", err)
  806. }
  807. for _, limit := range adjustedLimits {
  808. start := time.Now()
  809. // 构造URL
  810. url := fmt.Sprintf("%s/api/session/messages", svcCodeURL)
  811. // 构建请求体(POST方法)
  812. requestBody := map[string]interface{}{
  813. "sessionID": sessionID,
  814. }
  815. if limit > 0 {
  816. requestBody["limit"] = limit
  817. }
  818. jsonBody, err := json.Marshal(requestBody)
  819. if err != nil {
  820. t.Errorf("❌ 编码请求体失败 (limit=%d): %v", limit, err)
  821. continue
  822. }
  823. // 创建POST请求
  824. req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
  825. if err != nil {
  826. t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err)
  827. continue
  828. }
  829. // 添加认证头和内容类型
  830. req.Header.Set("Authorization", "Bearer "+token)
  831. req.Header.Set("Content-Type", "application/json")
  832. req.Header.Set("Accept", "application/json")
  833. // 发送请求
  834. client := &http.Client{Timeout: 30 * time.Second}
  835. resp, err := client.Do(req)
  836. if err != nil {
  837. t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err)
  838. continue
  839. }
  840. // 读取响应体
  841. body, err := io.ReadAll(resp.Body)
  842. resp.Body.Close()
  843. duration := time.Since(start)
  844. if err != nil {
  845. t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err)
  846. continue
  847. }
  848. if resp.StatusCode != http.StatusOK {
  849. t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d, 响应体: %s",
  850. limit, resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
  851. continue
  852. }
  853. // 解析响应
  854. var result struct {
  855. Success bool `json:"success"`
  856. Message string `json:"message"`
  857. Data struct {
  858. Messages []opencode.SessionMessage `json:"messages"`
  859. Count int `json:"count"`
  860. } `json:"data"`
  861. }
  862. if err := json.Unmarshal(body, &result); err != nil {
  863. t.Errorf("❌ 解析响应失败 (limit=%d): %v, 响应体: %s",
  864. limit, err, string(body[:minForMessagesTest(200, len(body))]))
  865. continue
  866. }
  867. if !result.Success {
  868. t.Errorf("❌ API调用失败 (limit=%d): %s", limit, result.Message)
  869. continue
  870. }
  871. actualCount := len(result.Data.Messages)
  872. t.Logf(" limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节",
  873. limit, duration, actualCount, len(body))
  874. }
  875. })
  876. // 3. 性能对比总结
  877. t.Run("PerformanceSummary", func(t *testing.T) {
  878. t.Log("📊 性能对比总结:")
  879. t.Log(" (具体数据见上述测试输出)")
  880. t.Log(" 📈 预期趋势:")
  881. t.Log(" 1. limit越小,响应时间越短")
  882. t.Log(" 2. 响应体大小与消息数量成正比")
  883. t.Log(" 3. svc-code API会有额外开销(认证、封装)")
  884. t.Log(" 💡 优化建议:")
  885. t.Log(" 1. 增量加载:只查询新消息(limit=新消息数量)")
  886. t.Log(" 2. 缓存:避免重复查询相同消息")
  887. t.Log(" 3. 前端过滤:即使查询全量,也可缓存过滤")
  888. })
  889. }