瀏覽代碼

Release v0.2.21301

qdy 2 週之前
當前提交
c0519416e9
共有 2 個文件被更改,包括 500 次插入3 次删除
  1. 22
    3
      internal/routes/session_messages_routes.go
  2. 478
    0
      test/session_messages_test.go

+ 22
- 3
internal/routes/session_messages_routes.go 查看文件

@@ -13,8 +13,8 @@ import (
13 13
 
14 14
 // GetSessionMessagesRequest 获取会话消息请求
15 15
 type GetSessionMessagesRequest struct {
16
-	SessionID string `form:"sessionID" binding:"required"`
17
-	Limit     int    `form:"limit,omitempty"`
16
+	SessionID string `json:"sessionID" binding:"required"`
17
+	Limit     int    `json:"limit,omitempty"`
18 18
 }
19 19
 
20 20
 // SessionMessagesResponse 会话消息响应
@@ -26,8 +26,18 @@ type SessionMessagesResponse struct {
26 26
 // RegisterSessionMessagesRoutes 注册会话消息路由
27 27
 func RegisterSessionMessagesRoutes(ws *router.RouterService, client opencode.OpenCodeClient) {
28 28
 	// 获取会话消息历史(需要Token认证)
29
-	ws.GET("/api/session/messages",
29
+	ws.POST("/api/session/messages",
30 30
 		func(req *GetSessionMessagesRequest, ctx context.Context, reqCtx *ctx.RequestContext) (*response.QueryResult[SessionMessagesResponse], error) {
31
+			// 检查请求参数是否为nil
32
+			if req == nil {
33
+				logger.Error("获取会话消息请求参数为nil")
34
+				return &response.QueryResult[SessionMessagesResponse]{
35
+					Success: false,
36
+					Message: "请求参数不能为空",
37
+				}, nil
38
+			}
39
+
40
+			// 检查sessionID是否为空
31 41
 			if req.SessionID == "" {
32 42
 				return &response.QueryResult[SessionMessagesResponse]{
33 43
 					Success: false,
@@ -35,6 +45,15 @@ func RegisterSessionMessagesRoutes(ws *router.RouterService, client opencode.Ope
35 45
 				}, nil
36 46
 			}
37 47
 
48
+			// 检查客户端是否可用
49
+			if client == nil {
50
+				logger.Error("opencode客户端未初始化", "session_id", req.SessionID)
51
+				return &response.QueryResult[SessionMessagesResponse]{
52
+					Success: false,
53
+					Message: "服务内部错误: opencode客户端未初始化",
54
+				}, nil
55
+			}
56
+
38 57
 			// 调用 opencode 客户端获取消息
39 58
 			messages, err := client.GetSessionMessages(ctx, req.SessionID, req.Limit)
40 59
 			if err != nil {

+ 478
- 0
test/session_messages_test.go 查看文件

@@ -0,0 +1,478 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"context"
6
+	"encoding/json"
7
+	"fmt"
8
+	"io"
9
+	"net/http"
10
+	"testing"
11
+	"time"
12
+
13
+	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
14
+)
15
+
16
+// TestDirectOpenCodeMessages 直接调用opencode API (8787端口) 获取会话消息
17
+func TestDirectOpenCodeMessages(t *testing.T) {
18
+	// 外部已启动的OpenCode服务端口
19
+	externalOpenCodePort := 8787
20
+	opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
21
+
22
+	// 检查OpenCode服务是否运行
23
+	if !isServiceRunningForMessages(t, opencodeURL) {
24
+		t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
25
+	}
26
+
27
+	t.Logf("🚀 开始测试直接调用OpenCode API获取消息")
28
+	t.Logf("OpenCode URL: %s", opencodeURL)
29
+
30
+	// 使用现有的测试会话ID
31
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
32
+	t.Logf("测试会话ID: %s", sessionID)
33
+
34
+	// 1. 使用DirectClient调用(测试SDK接口)
35
+	t.Run("SDKClient", func(t *testing.T) {
36
+		testDirectClientMessages(t, externalOpenCodePort, sessionID)
37
+	})
38
+
39
+	// 2. 直接HTTP调用(测试原始API)
40
+	t.Run("RawHTTP", func(t *testing.T) {
41
+		testRawHTTPMessages(t, opencodeURL, sessionID)
42
+	})
43
+}
44
+
45
+// testDirectClientMessages 使用DirectClient测试获取消息
46
+func testDirectClientMessages(t *testing.T, port int, sessionID string) {
47
+	// 创建客户端
48
+	client, err := opencode.NewDirectClient(port)
49
+	if err != nil {
50
+		t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
51
+	}
52
+	t.Logf("✅ 创建OpenCode客户端成功,基础URL: %s", client.GetBaseURL())
53
+
54
+	ctx := context.Background()
55
+
56
+	// 测试不带limit参数
57
+	t.Log("📝 测试获取消息(不带limit)")
58
+	messages, err := client.GetSessionMessages(ctx, sessionID, 0)
59
+	if err != nil {
60
+		t.Errorf("❌ 获取会话消息失败: %v", err)
61
+		return
62
+	}
63
+	t.Logf("✅ 获取到 %d 条消息", len(messages))
64
+
65
+	// 打印消息摘要
66
+	for i, msg := range messages {
67
+		t.Logf("  消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg))
68
+		if i < 3 && len(messages) > 3 { // 只打印前3条
69
+			if content := extractMessageContent(msg); content != "" {
70
+				t.Logf("     内容预览: %.100s...", content)
71
+			}
72
+		}
73
+	}
74
+
75
+	// 测试带limit参数
76
+	t.Log("📝 测试获取消息(limit=5)")
77
+	messagesLimited, err := client.GetSessionMessages(ctx, sessionID, 5)
78
+	if err != nil {
79
+		t.Errorf("❌ 获取限制数量的会话消息失败: %v", err)
80
+		return
81
+	}
82
+	t.Logf("✅ 获取到 %d 条消息(限制5条)", len(messagesLimited))
83
+
84
+	if len(messages) > 0 && len(messagesLimited) > 0 {
85
+		if len(messagesLimited) > 5 {
86
+			t.Errorf("❌ 消息数量超过限制: %d > 5", len(messagesLimited))
87
+		}
88
+	}
89
+}
90
+
91
+// testRawHTTPMessages 直接HTTP调用测试获取消息
92
+func testRawHTTPMessages(t *testing.T, baseURL, sessionID string) {
93
+	// 构造URL
94
+	url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID)
95
+	t.Logf("📡 调用原始API: %s", url)
96
+
97
+	// 发送HTTP请求
98
+	client := &http.Client{Timeout: 10 * time.Second}
99
+	req, err := http.NewRequest("GET", url, nil)
100
+	if err != nil {
101
+		t.Fatalf("❌ 创建请求失败: %v", err)
102
+	}
103
+	req.Header.Set("Accept", "application/json")
104
+
105
+	resp, err := client.Do(req)
106
+	if err != nil {
107
+		t.Fatalf("❌ HTTP请求失败: %v", err)
108
+	}
109
+	defer resp.Body.Close()
110
+
111
+	t.Logf("📊 响应状态码: %d", resp.StatusCode)
112
+
113
+	if resp.StatusCode != http.StatusOK {
114
+		body, _ := io.ReadAll(resp.Body)
115
+		t.Fatalf("❌ 获取会话消息失败,状态码: %d, 响应体: %s", resp.StatusCode, string(body))
116
+	}
117
+
118
+	// 解析响应
119
+	body, err := io.ReadAll(resp.Body)
120
+	if err != nil {
121
+		t.Fatalf("❌ 读取响应体失败: %v", err)
122
+	}
123
+
124
+	// 尝试解析为SessionMessage数组
125
+	var messages []opencode.SessionMessage
126
+	if err := json.Unmarshal(body, &messages); err != nil {
127
+		t.Errorf("❌ 解析消息响应失败: %v", err)
128
+		t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))]))
129
+		return
130
+	}
131
+
132
+	t.Logf("✅ 解析成功,获取到 %d 条消息", len(messages))
133
+
134
+	// 验证消息结构
135
+	for i, msg := range messages {
136
+		if extractMessageID(msg) == "" {
137
+			t.Errorf("❌ 消息[%d]缺少ID", i)
138
+		}
139
+		if extractMessageRole(msg) == "" {
140
+			t.Errorf("❌ 消息[%d]缺少角色", i)
141
+		}
142
+	}
143
+
144
+	// 测试带limit参数
145
+	urlWithLimit := fmt.Sprintf("%s?limit=3", url)
146
+	t.Logf("📡 调用带limit的API: %s", urlWithLimit)
147
+
148
+	reqLimit, err := http.NewRequest("GET", urlWithLimit, nil)
149
+	if err != nil {
150
+		t.Errorf("❌ 创建带limit的请求失败: %v", err)
151
+		return
152
+	}
153
+	reqLimit.Header.Set("Accept", "application/json")
154
+
155
+	respLimit, err := client.Do(reqLimit)
156
+	if err != nil {
157
+		t.Errorf("❌ HTTP请求(带limit)失败: %v", err)
158
+		return
159
+	}
160
+	defer respLimit.Body.Close()
161
+
162
+	if respLimit.StatusCode == http.StatusOK {
163
+		bodyLimit, _ := io.ReadAll(respLimit.Body)
164
+		var messagesLimit []opencode.SessionMessage
165
+		if err := json.Unmarshal(bodyLimit, &messagesLimit); err == nil {
166
+			t.Logf("✅ 带limit获取到 %d 条消息", len(messagesLimit))
167
+			if len(messagesLimit) > 3 {
168
+				t.Errorf("❌ 带limit的消息数量超过限制: %d > 3", len(messagesLimit))
169
+			}
170
+		}
171
+	}
172
+}
173
+
174
+// TestSvcCodeMessagesAPI 通过svc-code API (8020端口) 获取会话消息
175
+func TestSvcCodeMessagesAPI(t *testing.T) {
176
+	// svc-code服务地址
177
+	svcCodeURL := "http://localhost:8020"
178
+
179
+	// 检查svc-code服务是否运行
180
+	if !isServiceRunningForMessages(t, svcCodeURL) {
181
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
182
+	}
183
+
184
+	t.Logf("🚀 开始测试通过svc-code API获取消息")
185
+	t.Logf("svc-code URL: %s", svcCodeURL)
186
+
187
+	// 使用现有的测试会话ID
188
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
189
+	t.Logf("测试会话ID: %s", sessionID)
190
+
191
+	// 1. 用户登录获取token
192
+	token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL)
193
+	if err != nil {
194
+		t.Fatalf("❌ 登录失败: %v", err)
195
+	}
196
+	t.Logf("✅ 获取到Token: %s...", token[:minForMessagesTest(20, len(token))])
197
+
198
+	// 2. 测试不带limit参数
199
+	t.Run("WithoutLimit", func(t *testing.T) {
200
+		testSvcCodeMessages(t, svcCodeURL, token, sessionID, 0)
201
+	})
202
+
203
+	// 3. 测试带limit参数
204
+	t.Run("WithLimit", func(t *testing.T) {
205
+		testSvcCodeMessages(t, svcCodeURL, token, sessionID, 10)
206
+	})
207
+
208
+	// 4. 测试无效token
209
+	t.Run("InvalidToken", func(t *testing.T) {
210
+		testInvalidTokenForMessages(t, svcCodeURL, sessionID)
211
+	})
212
+
213
+	// 5. 测试无效sessionID
214
+	t.Run("InvalidSessionID", func(t *testing.T) {
215
+		testInvalidSessionIDForMessages(t, svcCodeURL, token)
216
+	})
217
+}
218
+
219
+// testSvcCodeMessages 测试svc-code获取消息API
220
+func testSvcCodeMessages(t *testing.T, baseURL, token, sessionID string, limit int) {
221
+	// 构造URL
222
+	url := fmt.Sprintf("%s/api/session/messages", baseURL)
223
+
224
+	// 创建请求
225
+	req, err := http.NewRequest("GET", url, nil)
226
+	if err != nil {
227
+		t.Fatalf("❌ 创建请求失败: %v", err)
228
+	}
229
+
230
+	// 添加查询参数
231
+	q := req.URL.Query()
232
+	q.Add("sessionID", sessionID)
233
+	if limit > 0 {
234
+		q.Add("limit", fmt.Sprintf("%d", limit))
235
+	}
236
+	req.URL.RawQuery = q.Encode()
237
+
238
+	// 添加认证头
239
+	req.Header.Set("Authorization", "Bearer "+token)
240
+	req.Header.Set("Accept", "application/json")
241
+
242
+	t.Logf("📡 调用svc-code API: %s", req.URL.String())
243
+
244
+	// 发送请求
245
+	client := &http.Client{Timeout: 10 * time.Second}
246
+	resp, err := client.Do(req)
247
+	if err != nil {
248
+		t.Fatalf("❌ HTTP请求失败: %v", err)
249
+	}
250
+	defer resp.Body.Close()
251
+
252
+	t.Logf("📊 响应状态码: %d", resp.StatusCode)
253
+
254
+	body, err := io.ReadAll(resp.Body)
255
+	if err != nil {
256
+		t.Fatalf("❌ 读取响应体失败: %v", err)
257
+	}
258
+
259
+	// 解析响应
260
+	var result struct {
261
+		Success bool   `json:"success"`
262
+		Message string `json:"message"`
263
+		Data    struct {
264
+			Messages []opencode.SessionMessage `json:"messages"`
265
+			Count    int                       `json:"count"`
266
+		} `json:"data"`
267
+	}
268
+
269
+	if err := json.Unmarshal(body, &result); err != nil {
270
+		t.Errorf("❌ 解析响应失败: %v", err)
271
+		t.Logf("原始响应: %s", string(body[:minForMessagesTest(500, len(body))]))
272
+		return
273
+	}
274
+
275
+	if !result.Success {
276
+		t.Errorf("❌ API调用失败: %s", result.Message)
277
+		return
278
+	}
279
+
280
+	t.Logf("✅ 获取成功!消息数量: %d, 总计: %d",
281
+		len(result.Data.Messages), result.Data.Count)
282
+
283
+	// 验证数据一致性
284
+	if result.Data.Count != len(result.Data.Messages) {
285
+		t.Errorf("❌ 数据不一致: Count=%d, Messages长度=%d",
286
+			result.Data.Count, len(result.Data.Messages))
287
+	}
288
+
289
+	// 打印消息摘要
290
+	for i, msg := range result.Data.Messages {
291
+		t.Logf("  消息[%d]: ID=%s, 角色=%s", i+1, extractMessageID(msg), extractMessageRole(msg))
292
+		if i < 2 && len(result.Data.Messages) > 2 {
293
+			if content := extractMessageContent(msg); content != "" {
294
+				t.Logf("     内容预览: %.100s...", content)
295
+			}
296
+		}
297
+	}
298
+
299
+	// 验证limit参数效果
300
+	if limit > 0 && len(result.Data.Messages) > limit {
301
+		t.Errorf("❌ 消息数量超过限制: %d > %d", len(result.Data.Messages), limit)
302
+	}
303
+}
304
+
305
+// testInvalidTokenForMessages 测试无效token
306
+func testInvalidTokenForMessages(t *testing.T, baseURL, sessionID string) {
307
+	url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, sessionID)
308
+
309
+	req, err := http.NewRequest("GET", url, nil)
310
+	if err != nil {
311
+		t.Fatalf("创建请求失败: %v", err)
312
+	}
313
+
314
+	// 使用无效token
315
+	req.Header.Set("Authorization", "Bearer invalid_token_12345")
316
+
317
+	client := &http.Client{Timeout: 5 * time.Second}
318
+	resp, err := client.Do(req)
319
+	if err != nil {
320
+		t.Fatalf("HTTP请求失败: %v", err)
321
+	}
322
+	defer resp.Body.Close()
323
+
324
+	// 应该返回401或403
325
+	if resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
326
+		t.Errorf("预期认证错误状态码(401/403),实际: %d", resp.StatusCode)
327
+		body, _ := io.ReadAll(resp.Body)
328
+		t.Logf("响应体: %s", string(body))
329
+	} else {
330
+		t.Logf("✅ 无效token测试通过,返回状态码: %d", resp.StatusCode)
331
+	}
332
+}
333
+
334
+// testInvalidSessionIDForMessages 测试无效sessionID
335
+func testInvalidSessionIDForMessages(t *testing.T, baseURL, token string) {
336
+	invalidSessionID := "ses_invalid_12345"
337
+	url := fmt.Sprintf("%s/api/session/messages?sessionID=%s", baseURL, invalidSessionID)
338
+
339
+	req, err := http.NewRequest("GET", url, nil)
340
+	if err != nil {
341
+		t.Fatalf("创建请求失败: %v", err)
342
+	}
343
+
344
+	req.Header.Set("Authorization", "Bearer "+token)
345
+
346
+	client := &http.Client{Timeout: 5 * time.Second}
347
+	resp, err := client.Do(req)
348
+	if err != nil {
349
+		t.Fatalf("HTTP请求失败: %v", err)
350
+	}
351
+	defer resp.Body.Close()
352
+
353
+	body, err := io.ReadAll(resp.Body)
354
+	if err != nil {
355
+		t.Fatalf("读取响应体失败: %v", err)
356
+	}
357
+
358
+	var result struct {
359
+		Success bool   `json:"success"`
360
+		Message string `json:"message"`
361
+	}
362
+
363
+	if err := json.Unmarshal(body, &result); err != nil {
364
+		t.Errorf("解析响应失败: %v", err)
365
+		return
366
+	}
367
+
368
+	if result.Success {
369
+		t.Errorf("无效sessionID应该返回失败,实际成功")
370
+	} else {
371
+		t.Logf("✅ 无效sessionID测试通过,错误信息: %s", result.Message)
372
+	}
373
+}
374
+
375
+// loginAndGetTokenForMessagesTest 登录获取token
376
+func loginAndGetTokenForMessagesTest(t *testing.T, baseURL string) (string, error) {
377
+	loginURL := baseURL + "/api/auth/login"
378
+	loginData := map[string]string{
379
+		"user_id":  "test-user-001",
380
+		"password": "password123",
381
+	}
382
+	jsonData, _ := json.Marshal(loginData)
383
+
384
+	resp, err := http.Post(loginURL, "application/json", bytes.NewBuffer(jsonData))
385
+	if err != nil {
386
+		return "", fmt.Errorf("登录请求失败: %v", err)
387
+	}
388
+	defer resp.Body.Close()
389
+
390
+	if resp.StatusCode != http.StatusOK {
391
+		bodyBytes, _ := io.ReadAll(resp.Body)
392
+		return "", fmt.Errorf("登录失败 (状态码 %d): %s", resp.StatusCode, string(bodyBytes))
393
+	}
394
+
395
+	var result struct {
396
+		Success bool   `json:"success"`
397
+		Data    string `json:"data"`
398
+		Message string `json:"message"`
399
+	}
400
+
401
+	bodyBytes, _ := io.ReadAll(resp.Body)
402
+	if err := json.Unmarshal(bodyBytes, &result); err != nil {
403
+		return "", fmt.Errorf("解析响应失败: %v", err)
404
+	}
405
+
406
+	if !result.Success {
407
+		return "", fmt.Errorf("登录失败: %s", result.Message)
408
+	}
409
+
410
+	return result.Data, nil
411
+}
412
+
413
+// isServiceRunningForMessages 检查服务是否运行
414
+func isServiceRunningForMessages(t *testing.T, url string) bool {
415
+	client := &http.Client{Timeout: 3 * time.Second}
416
+
417
+	// 尝试多个端点
418
+	endpoints := []string{
419
+		"/global/health",   // opencode健康检查
420
+		"/api/auth/health", // svc-code健康检查
421
+		"/api/health",      // 其他健康检查
422
+		"",                 // 根路径
423
+	}
424
+
425
+	for _, endpoint := range endpoints {
426
+		fullURL := url + endpoint
427
+		resp, err := client.Get(fullURL)
428
+		if err == nil {
429
+			resp.Body.Close()
430
+			if resp.StatusCode == http.StatusOK {
431
+				t.Logf("✅ 服务运行正常: %s (状态码: %d)", fullURL, resp.StatusCode)
432
+				return true
433
+			}
434
+		}
435
+	}
436
+
437
+	return false
438
+}
439
+
440
+// extractMessageID 从SessionMessage提取消息ID
441
+func extractMessageID(msg opencode.SessionMessage) string {
442
+	if msg.Info != nil {
443
+		if id, ok := msg.Info["id"].(string); ok {
444
+			return id
445
+		}
446
+	}
447
+	return ""
448
+}
449
+
450
+// extractMessageRole 从SessionMessage提取消息角色
451
+func extractMessageRole(msg opencode.SessionMessage) string {
452
+	if msg.Info != nil {
453
+		if role, ok := msg.Info["role"].(string); ok {
454
+			return role
455
+		}
456
+	}
457
+	return ""
458
+}
459
+
460
+// extractMessageContent 从SessionMessage提取消息内容
461
+func extractMessageContent(msg opencode.SessionMessage) string {
462
+	if len(msg.Parts) > 0 {
463
+		for _, part := range msg.Parts {
464
+			if text, ok := part["text"].(string); ok && text != "" {
465
+				return text
466
+			}
467
+		}
468
+	}
469
+	return ""
470
+}
471
+
472
+// minForMessagesTest 返回两个整数的最小值
473
+func minForMessagesTest(a, b int) int {
474
+	if a < b {
475
+		return a
476
+	}
477
+	return b
478
+}

Loading…
取消
儲存