Просмотр исходного кода

页签切换可以了-优化过一次

qdy 2 недель назад
Родитель
Сommit
3238bd12a2
2 измененных файлов: 949 добавлений и 0 удалений
  1. 383
    0
      test/performance_benchmark_test.go
  2. 566
    0
      test/session_messages_test.go

+ 383
- 0
test/performance_benchmark_test.go Просмотреть файл

@@ -0,0 +1,383 @@
1
+package main
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io"
8
+	"net/http"
9
+	"testing"
10
+	"time"
11
+
12
+	"git.x2erp.com/qdy/go-svc-code/internal/opencode"
13
+)
14
+
15
+// TestMessageResponseTimeComparison 性能对比测试:比较8787端口和8020端口的历史消息加载性能
16
+func TestMessageResponseTimeComparison(t *testing.T) {
17
+	// 测试会话ID - 使用现有会话
18
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
19
+	t.Logf("🚀 开始性能对比测试")
20
+	t.Logf("测试会话ID: %s", sessionID)
21
+
22
+	// 1. 测试8787端口(直接opencode服务)
23
+	t.Run("DirectOpenCode_8787", func(t *testing.T) {
24
+		directURL := "http://localhost:8787"
25
+
26
+		// 检查服务是否可用
27
+		if !isServiceRunningForMessages(t, directURL) {
28
+			t.Skipf("8787端口服务不可用,跳过测试")
29
+			return
30
+		}
31
+
32
+		t.Logf("📊 测试8787端口直接访问...")
33
+
34
+		// 测试不同消息数量的加载性能
35
+		testCases := []struct {
36
+			name   string
37
+			limit  int
38
+			repeat int // 重复次数
39
+		}{
40
+			{"limit_10", 10, 5},
41
+			{"limit_50", 50, 5},
42
+			{"limit_100", 100, 5},
43
+			{"no_limit", 0, 3},
44
+		}
45
+
46
+		for _, tc := range testCases {
47
+			t.Run(tc.name, func(t *testing.T) {
48
+				var totalDuration time.Duration
49
+				var successes int
50
+				var firstByteTimes []time.Duration
51
+
52
+				for i := 0; i < tc.repeat; i++ {
53
+					duration, firstByte, err := measureDirectRequest(directURL, sessionID, tc.limit)
54
+					if err != nil {
55
+						t.Logf("⚠️  第%d次请求失败: %v", i+1, err)
56
+						continue
57
+					}
58
+
59
+					totalDuration += duration
60
+					firstByteTimes = append(firstByteTimes, firstByte)
61
+					successes++
62
+
63
+					// 请求间短暂延迟
64
+					time.Sleep(100 * time.Millisecond)
65
+				}
66
+
67
+				if successes > 0 {
68
+					avgDuration := totalDuration / time.Duration(successes)
69
+					var avgFirstByte time.Duration
70
+					for _, fb := range firstByteTimes {
71
+						avgFirstByte += fb
72
+					}
73
+					avgFirstByte /= time.Duration(len(firstByteTimes))
74
+
75
+					t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v",
76
+						tc.name, successes, tc.repeat, avgDuration, avgFirstByte)
77
+				} else {
78
+					t.Errorf("❌ %s: 所有请求都失败", tc.name)
79
+				}
80
+			})
81
+		}
82
+	})
83
+
84
+	// 2. 测试8020端口(svc-code代理)
85
+	t.Run("SvcCodeProxy_8020", func(t *testing.T) {
86
+		proxyURL := "http://localhost:8020"
87
+
88
+		// 检查服务是否可用
89
+		if !isServiceRunningForMessages(t, proxyURL) {
90
+			t.Skipf("8020端口服务不可用,跳过测试")
91
+			return
92
+		}
93
+
94
+		// 获取认证token
95
+		token, err := loginAndGetTokenForMessagesTest(t, proxyURL)
96
+		if err != nil {
97
+			t.Skipf("获取认证token失败: %v,跳过8020端口测试", err)
98
+			return
99
+		}
100
+
101
+		t.Logf("📊 测试8020端口代理访问...")
102
+		t.Logf("Token获取成功(前20位): %s...", token[:minForMessagesTest(20, len(token))])
103
+
104
+		// 测试不同消息数量的加载性能
105
+		testCases := []struct {
106
+			name   string
107
+			limit  int
108
+			repeat int // 重复次数
109
+		}{
110
+			{"limit_10", 10, 5},
111
+			{"limit_50", 50, 5},
112
+			{"limit_100", 100, 5},
113
+			{"no_limit", 0, 3},
114
+		}
115
+
116
+		for _, tc := range testCases {
117
+			t.Run(tc.name, func(t *testing.T) {
118
+				var totalDuration time.Duration
119
+				var successes int
120
+				var firstByteTimes []time.Duration
121
+
122
+				for i := 0; i < tc.repeat; i++ {
123
+					duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, tc.limit)
124
+					if err != nil {
125
+						t.Logf("⚠️  第%d次请求失败: %v", i+1, err)
126
+						continue
127
+					}
128
+
129
+					totalDuration += duration
130
+					firstByteTimes = append(firstByteTimes, firstByte)
131
+					successes++
132
+
133
+					// 请求间短暂延迟
134
+					time.Sleep(100 * time.Millisecond)
135
+				}
136
+
137
+				if successes > 0 {
138
+					avgDuration := totalDuration / time.Duration(successes)
139
+					var avgFirstByte time.Duration
140
+					for _, fb := range firstByteTimes {
141
+						avgFirstByte += fb
142
+					}
143
+					avgFirstByte /= time.Duration(len(firstByteTimes))
144
+
145
+					t.Logf("✅ %s: 成功%d/%d次, 平均总时间: %v, 平均首字节: %v",
146
+						tc.name, successes, tc.repeat, avgDuration, avgFirstByte)
147
+				} else {
148
+					t.Errorf("❌ %s: 所有请求都失败", tc.name)
149
+				}
150
+			})
151
+		}
152
+	})
153
+}
154
+
155
+// TestPerformanceSummary 性能对比总结
156
+func TestPerformanceSummary(t *testing.T) {
157
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
158
+
159
+	// 只测试limit=50的情况,进行更详细的对比
160
+	t.Logf("📈 性能对比总结测试 (limit=50)")
161
+
162
+	// 测试8787端口
163
+	directTimes := []time.Duration{}
164
+	directFirstBytes := []time.Duration{}
165
+	directURL := "http://localhost:8787"
166
+
167
+	if isServiceRunningForMessages(t, directURL) {
168
+		t.Log("🔍 测试8787端口...")
169
+		for i := 0; i < 10; i++ {
170
+			duration, firstByte, err := measureDirectRequest(directURL, sessionID, 50)
171
+			if err != nil {
172
+				t.Logf("⚠️  8787端口第%d次请求失败: %v", i+1, err)
173
+				continue
174
+			}
175
+			directTimes = append(directTimes, duration)
176
+			directFirstBytes = append(directFirstBytes, firstByte)
177
+			time.Sleep(50 * time.Millisecond)
178
+		}
179
+	}
180
+
181
+	// 测试8020端口
182
+	proxyTimes := []time.Duration{}
183
+	proxyFirstBytes := []time.Duration{}
184
+	proxyURL := "http://localhost:8020"
185
+	token := ""
186
+
187
+	if isServiceRunningForMessages(t, proxyURL) {
188
+		t.Log("🔍 测试8020端口...")
189
+		token, _ = loginAndGetTokenForMessagesTest(t, proxyURL)
190
+		if token != "" {
191
+			for i := 0; i < 10; i++ {
192
+				duration, firstByte, err := measureProxyRequest(proxyURL, sessionID, token, 50)
193
+				if err != nil {
194
+					t.Logf("⚠️  8020端口第%d次请求失败: %v", i+1, err)
195
+					continue
196
+				}
197
+				proxyTimes = append(proxyTimes, duration)
198
+				proxyFirstBytes = append(proxyFirstBytes, firstByte)
199
+				time.Sleep(50 * time.Millisecond)
200
+			}
201
+		}
202
+	}
203
+
204
+	// 分析结果
205
+	if len(directTimes) > 0 && len(proxyTimes) > 0 {
206
+		directAvg := averageDuration(directTimes)
207
+		directFBAvg := averageDuration(directFirstBytes)
208
+		proxyAvg := averageDuration(proxyTimes)
209
+		proxyFBAvg := averageDuration(proxyFirstBytes)
210
+
211
+		diff := proxyAvg - directAvg
212
+		diffPercent := float64(diff) / float64(directAvg) * 100
213
+
214
+		t.Logf("\n📊 性能对比结果:")
215
+		t.Logf("   8787端口(直接): 平均总时间=%v, 平均首字节=%v", directAvg, directFBAvg)
216
+		t.Logf("   8020端口(代理): 平均总时间=%v, 平均首字节=%v", proxyAvg, proxyFBAvg)
217
+		t.Logf("   差异: %v (%.1f%%)", diff, diffPercent)
218
+
219
+		if diffPercent > 20 {
220
+			t.Logf("⚠️  8020端口比8787端口慢%.1f%%,可能存在代理开销", diffPercent)
221
+		} else if diffPercent > 50 {
222
+			t.Logf("❌ 8020端口性能显著下降,建议优化代理逻辑")
223
+		} else if diffPercent < -10 {
224
+			t.Logf("✅ 8020端口性能更好,可能包含缓存优化")
225
+		} else {
226
+			t.Logf("✅ 两端性能接近,差异在可接受范围内")
227
+		}
228
+	} else {
229
+		t.Logf("⚠️  无法完成完整对比,部分端口数据缺失")
230
+		if len(directTimes) == 0 {
231
+			t.Logf("   - 8787端口无有效数据")
232
+		}
233
+		if len(proxyTimes) == 0 {
234
+			t.Logf("   - 8020端口无有效数据")
235
+		}
236
+	}
237
+}
238
+
239
+// measureDirectRequest 测量直接访问8787端口的请求性能
240
+func measureDirectRequest(baseURL, sessionID string, limit int) (totalDuration, firstByteDuration time.Duration, err error) {
241
+	// 构造URL
242
+	url := fmt.Sprintf("%s/session/%s/message", baseURL, sessionID)
243
+	if limit > 0 {
244
+		url = fmt.Sprintf("%s?limit=%d", url, limit)
245
+	}
246
+
247
+	client := &http.Client{
248
+		Timeout: 30 * time.Second,
249
+	}
250
+
251
+	req, err := http.NewRequest("GET", url, nil)
252
+	if err != nil {
253
+		return 0, 0, fmt.Errorf("创建请求失败: %w", err)
254
+	}
255
+	req.Header.Set("Accept", "application/json")
256
+
257
+	startTime := time.Now()
258
+
259
+	// 监听响应首字节
260
+	req.Header.Set("Accept-Encoding", "identity") // 禁用压缩以便准确测量
261
+
262
+	resp, err := client.Do(req)
263
+	if err != nil {
264
+		return 0, 0, fmt.Errorf("HTTP请求失败: %w", err)
265
+	}
266
+	defer resp.Body.Close()
267
+
268
+	// 记录首字节到达时间
269
+	firstByteTime := time.Now()
270
+	firstByteDuration = firstByteTime.Sub(startTime)
271
+
272
+	// 读取完整响应体
273
+	body, err := io.ReadAll(resp.Body)
274
+	if err != nil {
275
+		return 0, 0, fmt.Errorf("读取响应体失败: %w", err)
276
+	}
277
+
278
+	totalDuration = time.Since(startTime)
279
+
280
+	// 验证响应状态
281
+	if resp.StatusCode != http.StatusOK {
282
+		return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
283
+	}
284
+
285
+	// 验证响应格式
286
+	var messages []opencode.SessionMessage
287
+	if err := json.Unmarshal(body, &messages); err != nil {
288
+		return 0, 0, fmt.Errorf("解析响应失败: %w", err)
289
+	}
290
+
291
+	return totalDuration, firstByteDuration, nil
292
+}
293
+
294
+// measureProxyRequest 测量通过8020端口代理的请求性能
295
+func measureProxyRequest(baseURL, sessionID, token string, limit int) (totalDuration, firstByteDuration time.Duration, err error) {
296
+	// 构造URL
297
+	url := fmt.Sprintf("%s/api/session/messages", baseURL)
298
+
299
+	// 创建请求体
300
+	requestBody := map[string]interface{}{
301
+		"sessionID": sessionID,
302
+	}
303
+	if limit > 0 {
304
+		requestBody["limit"] = limit
305
+	}
306
+
307
+	jsonBody, err := json.Marshal(requestBody)
308
+	if err != nil {
309
+		return 0, 0, fmt.Errorf("编码请求体失败: %w", err)
310
+	}
311
+
312
+	client := &http.Client{
313
+		Timeout: 30 * time.Second,
314
+	}
315
+
316
+	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
317
+	if err != nil {
318
+		return 0, 0, fmt.Errorf("创建请求失败: %w", err)
319
+	}
320
+
321
+	// 添加认证头
322
+	req.Header.Set("Authorization", "Bearer "+token)
323
+	req.Header.Set("Accept", "application/json")
324
+	req.Header.Set("Content-Type", "application/json")
325
+	req.Header.Set("Accept-Encoding", "identity")
326
+
327
+	startTime := time.Now()
328
+
329
+	resp, err := client.Do(req)
330
+	if err != nil {
331
+		return 0, 0, fmt.Errorf("HTTP请求失败: %w", err)
332
+	}
333
+	defer resp.Body.Close()
334
+
335
+	// 记录首字节到达时间
336
+	firstByteTime := time.Now()
337
+	firstByteDuration = firstByteTime.Sub(startTime)
338
+
339
+	// 读取完整响应体
340
+	body, err := io.ReadAll(resp.Body)
341
+	if err != nil {
342
+		return 0, 0, fmt.Errorf("读取响应体失败: %w", err)
343
+	}
344
+
345
+	totalDuration = time.Since(startTime)
346
+
347
+	// 验证响应状态
348
+	if resp.StatusCode != http.StatusOK {
349
+		return 0, 0, fmt.Errorf("状态码错误: %d, 响应: %s", resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
350
+	}
351
+
352
+	// 验证响应格式
353
+	var result struct {
354
+		Success bool   `json:"success"`
355
+		Message string `json:"message"`
356
+		Data    struct {
357
+			Messages []opencode.SessionMessage `json:"messages"`
358
+			Count    int                       `json:"count"`
359
+		} `json:"data"`
360
+	}
361
+
362
+	if err := json.Unmarshal(body, &result); err != nil {
363
+		return 0, 0, fmt.Errorf("解析响应失败: %w", err)
364
+	}
365
+
366
+	if !result.Success {
367
+		return 0, 0, fmt.Errorf("API调用失败: %s", result.Message)
368
+	}
369
+
370
+	return totalDuration, firstByteDuration, nil
371
+}
372
+
373
+// averageDuration 计算平均时长
374
+func averageDuration(durations []time.Duration) time.Duration {
375
+	if len(durations) == 0 {
376
+		return 0
377
+	}
378
+	var total time.Duration
379
+	for _, d := range durations {
380
+		total += d
381
+	}
382
+	return total / time.Duration(len(durations))
383
+}

+ 566
- 0
test/session_messages_test.go Просмотреть файл

@@ -476,3 +476,569 @@ func minForMessagesTest(a, b int) int {
476 476
 	}
477 477
 	return b
478 478
 }
479
+
480
+// extractTimestamp 从SessionMessage提取时间戳
481
+func extractTimestamp(msg opencode.SessionMessage) (time.Time, error) {
482
+	// 尝试从info.time.created提取
483
+	if msg.Info != nil {
484
+		// 调试:打印info的所有键
485
+		// fmt.Printf("Info keys: %v\n", getMapKeys(msg.Info))
486
+
487
+		// 1. 尝试从time.created提取(毫秒时间戳)
488
+		if timeMap, ok := msg.Info["time"].(map[string]interface{}); ok {
489
+			// fmt.Printf("Time map keys: %v\n", getMapKeys(timeMap))
490
+
491
+			// 尝试毫秒时间戳(整数)
492
+			if createdMs, ok := timeMap["created"].(float64); ok {
493
+				// 转换为秒(除以1000)
494
+				seconds := int64(createdMs) / 1000
495
+				nanoseconds := (int64(createdMs) % 1000) * 1000000
496
+				return time.Unix(seconds, nanoseconds), nil
497
+			}
498
+			// 尝试字符串格式
499
+			if createdStr, ok := timeMap["created"].(string); ok {
500
+				return time.Parse(time.RFC3339, createdStr)
501
+			}
502
+		}
503
+		// 2. 尝试直接提取created(毫秒时间戳)
504
+		if createdMs, ok := msg.Info["created"].(float64); ok {
505
+			seconds := int64(createdMs) / 1000
506
+			nanoseconds := (int64(createdMs) % 1000) * 1000000
507
+			return time.Unix(seconds, nanoseconds), nil
508
+		}
509
+		// 3. 尝试从created_at提取(字符串或数字)
510
+		if createdVal, ok := msg.Info["created_at"]; ok {
511
+			switch v := createdVal.(type) {
512
+			case float64:
513
+				seconds := int64(v) / 1000
514
+				nanoseconds := (int64(v) % 1000) * 1000000
515
+				return time.Unix(seconds, nanoseconds), nil
516
+			case string:
517
+				return time.Parse(time.RFC3339, v)
518
+			}
519
+		}
520
+		// 4. 尝试从createdAt提取(驼峰式)
521
+		if createdAtVal, ok := msg.Info["createdAt"]; ok {
522
+			switch v := createdAtVal.(type) {
523
+			case float64:
524
+				seconds := int64(v) / 1000
525
+				nanoseconds := (int64(v) % 1000) * 1000000
526
+				return time.Unix(seconds, nanoseconds), nil
527
+			case string:
528
+				return time.Parse(time.RFC3339, v)
529
+			}
530
+		}
531
+		// 5. 尝试从timestamp提取
532
+		if timestampFloat, ok := msg.Info["timestamp"].(float64); ok {
533
+			return time.Unix(int64(timestampFloat), 0), nil
534
+		}
535
+		// 6. 尝试从date提取
536
+		if dateStr, ok := msg.Info["date"].(string); ok {
537
+			return time.Parse(time.RFC3339, dateStr)
538
+		}
539
+		// 7. 尝试从time直接提取(可能是字符串)
540
+		if timeStr, ok := msg.Info["time"].(string); ok {
541
+			return time.Parse(time.RFC3339, timeStr)
542
+		}
543
+	}
544
+	return time.Time{}, fmt.Errorf("无法提取时间戳")
545
+}
546
+
547
+// getMapKeys 获取map的所有键(用于调试)
548
+func getMapKeys(m map[string]interface{}) []string {
549
+	keys := make([]string, 0, len(m))
550
+	for k := range m {
551
+		keys = append(keys, k)
552
+	}
553
+	return keys
554
+}
555
+
556
+// TestMessageFormatAndSorting 测试消息格式和排序规则
557
+func TestMessageFormatAndSorting(t *testing.T) {
558
+	// 外部已启动的OpenCode服务端口
559
+	externalOpenCodePort := 8787
560
+	opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
561
+
562
+	// 检查OpenCode服务是否运行
563
+	if !isServiceRunningForMessages(t, opencodeURL) {
564
+		t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
565
+	}
566
+
567
+	t.Logf("🚀 开始测试消息格式和排序规则")
568
+	t.Logf("OpenCode URL: %s", opencodeURL)
569
+
570
+	// 使用现有的测试会话ID
571
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
572
+	t.Logf("测试会话ID: %s", sessionID)
573
+
574
+	// 创建客户端
575
+	client, err := opencode.NewDirectClient(externalOpenCodePort)
576
+	if err != nil {
577
+		t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
578
+	}
579
+
580
+	ctx := context.Background()
581
+
582
+	// 获取消息(不带limit)
583
+	t.Log("📝 获取消息进行格式和排序分析")
584
+	messages, err := client.GetSessionMessages(ctx, sessionID, 0)
585
+	if err != nil {
586
+		t.Fatalf("❌ 获取会话消息失败: %v", err)
587
+	}
588
+
589
+	t.Logf("✅ 获取到 %d 条消息进行分析", len(messages))
590
+
591
+	if len(messages) == 0 {
592
+		t.Skip("会话中没有消息,跳过格式和排序测试")
593
+	}
594
+
595
+	// 1. 验证消息格式完整性
596
+	t.Run("MessageFormatIntegrity", func(t *testing.T) {
597
+		for i, msg := range messages {
598
+			// 检查消息ID
599
+			msgID := extractMessageID(msg)
600
+			if msgID == "" {
601
+				t.Errorf("❌ 消息[%d]缺少ID", i)
602
+			}
603
+
604
+			// 检查角色
605
+			role := extractMessageRole(msg)
606
+			if role == "" {
607
+				t.Errorf("❌ 消息[%d]缺少角色", i)
608
+			}
609
+
610
+			// 检查内容
611
+			content := extractMessageContent(msg)
612
+			if content == "" && role != "system" {
613
+				t.Logf("⚠️  消息[%d] (ID: %s, 角色: %s) 内容为空", i, msgID, role)
614
+			}
615
+
616
+			// 检查时间戳
617
+			timestamp, err := extractTimestamp(msg)
618
+			if err != nil {
619
+				t.Logf("⚠️  消息[%d] (ID: %s) 无法提取时间戳: %v", i, msgID, err)
620
+			} else {
621
+				t.Logf("   消息[%d] 时间戳: %v", i, timestamp.Format("2006-01-02 15:04:05"))
622
+			}
623
+		}
624
+	})
625
+
626
+	// 2. 验证消息排序(时间倒序:最新在前)
627
+	t.Run("MessageSortingOrder", func(t *testing.T) {
628
+		if len(messages) < 2 {
629
+			t.Skip("消息数量不足,跳过排序测试")
630
+		}
631
+
632
+		// 收集所有有效的时间戳
633
+		var timestamps []time.Time
634
+		var validMessages []opencode.SessionMessage
635
+
636
+		for i, msg := range messages {
637
+			timestamp, err := extractTimestamp(msg)
638
+			if err == nil {
639
+				timestamps = append(timestamps, timestamp)
640
+				validMessages = append(validMessages, msg)
641
+			} else {
642
+				t.Logf("⚠️  消息[%d] 跳过排序检查(无有效时间戳)", i)
643
+			}
644
+		}
645
+
646
+		if len(timestamps) < 2 {
647
+			t.Skip("有效时间戳数量不足,跳过排序测试")
648
+		}
649
+
650
+		// 检查排序方向
651
+		// 先确定排序方向:检查前几对消息
652
+		var isDescending bool
653
+		if len(timestamps) >= 2 {
654
+			// 检查前几对确定方向
655
+			descendingCount := 0
656
+			ascendingCount := 0
657
+			for i := 1; i < minForMessagesTest(5, len(timestamps)); i++ {
658
+				if timestamps[i].Before(timestamps[i-1]) {
659
+					descendingCount++ // 时间递减(最新在前)
660
+				} else if timestamps[i].After(timestamps[i-1]) {
661
+					ascendingCount++ // 时间递增(最早在前)
662
+				}
663
+			}
664
+
665
+			if descendingCount > ascendingCount {
666
+				isDescending = true
667
+				t.Logf("✅ 消息按时间倒序排列(最新在前)")
668
+			} else if ascendingCount > descendingCount {
669
+				isDescending = false
670
+				t.Logf("✅ 消息按时间顺序排列(最早在前)")
671
+			} else {
672
+				t.Logf("⚠️  无法确定排序方向,可能时间戳相同或乱序")
673
+				isDescending = false
674
+			}
675
+
676
+			// 验证排序一致性
677
+			if isDescending {
678
+				// 应该时间递减
679
+				for i := 1; i < len(timestamps); i++ {
680
+					if timestamps[i].After(timestamps[i-1]) {
681
+						t.Logf("❌ 倒序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更晚",
682
+							i, timestamps[i].Format("15:04:05"),
683
+							i-1, timestamps[i-1].Format("15:04:05"))
684
+					}
685
+				}
686
+			} else {
687
+				// 应该时间递增
688
+				for i := 1; i < len(timestamps); i++ {
689
+					if timestamps[i].Before(timestamps[i-1]) {
690
+						t.Logf("❌ 顺序不一致: 消息[%d] (%v) 比消息[%d] (%v) 更早",
691
+							i, timestamps[i].Format("15:04:05"),
692
+							i-1, timestamps[i-1].Format("15:04:05"))
693
+					}
694
+				}
695
+			}
696
+		}
697
+
698
+		if isDescending {
699
+			t.Logf("✅ 消息按时间倒序排列(最新在前)")
700
+			// 打印时间戳范围
701
+			if len(timestamps) > 0 {
702
+				firstTime := timestamps[0]
703
+				lastTime := timestamps[len(timestamps)-1]
704
+				t.Logf("📊 时间戳范围:")
705
+				t.Logf("    第一条消息: %v (%v)", firstTime.Format("2006-01-02 15:04:05"), firstTime.Unix())
706
+				t.Logf("    最后一条消息: %v (%v)", lastTime.Format("2006-01-02 15:04:05"), lastTime.Unix())
707
+				t.Logf("    时间跨度: %v", lastTime.Sub(firstTime))
708
+
709
+				// 打印前3条和后3条
710
+				t.Logf("    前3条消息时间:")
711
+				for i := 0; i < minForMessagesTest(3, len(timestamps)); i++ {
712
+					t.Logf("      [%d]: %v", i, timestamps[i].Format("15:04:05"))
713
+				}
714
+				if len(timestamps) > 6 {
715
+					t.Logf("    后3条消息时间:")
716
+					for i := len(timestamps) - 3; i < len(timestamps); i++ {
717
+						t.Logf("      [%d]: %v", i, timestamps[i].Format("15:04:05"))
718
+					}
719
+				}
720
+			}
721
+		}
722
+		// 不因为排序方向而失败,只是记录信息
723
+		if !isDescending {
724
+			t.Logf("ℹ️  消息按时间顺序排列(最早在前),这对于增量加载是好事")
725
+		}
726
+	})
727
+
728
+	// 3. 验证limit参数对排序的影响
729
+	t.Run("LimitParameterEffect", func(t *testing.T) {
730
+		// 获取带limit的消息
731
+		limit := 5
732
+		if len(messages) < limit {
733
+			limit = len(messages)
734
+		}
735
+
736
+		limitedMessages, err := client.GetSessionMessages(ctx, sessionID, limit)
737
+		if err != nil {
738
+			t.Fatalf("❌ 获取限制数量的消息失败: %v", err)
739
+		}
740
+
741
+		t.Logf("✅ 获取到 %d 条限制消息", len(limitedMessages))
742
+
743
+		// 检查limit是否有效
744
+		if len(limitedMessages) > limit {
745
+			t.Errorf("❌ limit参数无效: 返回 %d 条消息,限制为 %d", len(limitedMessages), limit)
746
+		}
747
+
748
+		// 检查limit消息是否与原消息前N条一致
749
+		if len(limitedMessages) > 0 && len(messages) >= len(limitedMessages) {
750
+			for i := 0; i < len(limitedMessages); i++ {
751
+				limitedID := extractMessageID(limitedMessages[i])
752
+				fullID := extractMessageID(messages[i])
753
+
754
+				if limitedID != fullID {
755
+					t.Errorf("❌ limit消息不匹配: 位置[%d] limit消息ID=%s, 全量消息ID=%s",
756
+						i, limitedID, fullID)
757
+					break
758
+				}
759
+			}
760
+			if t.Failed() {
761
+				t.Logf("⚠️  limit消息与全量消息前N条不一致")
762
+			} else {
763
+				t.Logf("✅ limit参数正确返回前%d条消息", limit)
764
+			}
765
+		}
766
+	})
767
+
768
+	// 4. 输出消息格式摘要
769
+	t.Run("MessageFormatSummary", func(t *testing.T) {
770
+		t.Logf("📋 消息格式摘要:")
771
+		t.Logf("   消息总数: %d", len(messages))
772
+
773
+		// 角色统计
774
+		roleCount := make(map[string]int)
775
+		for _, msg := range messages {
776
+			role := extractMessageRole(msg)
777
+			roleCount[role]++
778
+		}
779
+		for role, count := range roleCount {
780
+			t.Logf("   角色 '%s': %d 条", role, count)
781
+		}
782
+
783
+		// 时间戳可用性统计
784
+		timestampCount := 0
785
+		for _, msg := range messages {
786
+			_, err := extractTimestamp(msg)
787
+			if err == nil {
788
+				timestampCount++
789
+			}
790
+		}
791
+		t.Logf("   有效时间戳: %d/%d (%.1f%%)", timestampCount, len(messages),
792
+			float64(timestampCount)/float64(len(messages))*100)
793
+
794
+		// 内容长度统计
795
+		totalContentLength := 0
796
+		for _, msg := range messages {
797
+			content := extractMessageContent(msg)
798
+			totalContentLength += len(content)
799
+		}
800
+		t.Logf("   总内容长度: %d 字符", totalContentLength)
801
+		if len(messages) > 0 {
802
+			t.Logf("   平均内容长度: %.1f 字符", float64(totalContentLength)/float64(len(messages)))
803
+		}
804
+
805
+		// 打印第一条消息的完整结构(用于调试)
806
+		if len(messages) > 0 {
807
+			t.Logf("🔍 第一条消息完整结构:")
808
+			firstMsg := messages[0]
809
+			if firstMsg.Info != nil {
810
+				infoJSON, _ := json.MarshalIndent(firstMsg.Info, "      ", "  ")
811
+				t.Logf("     Info: %s", infoJSON)
812
+				// 打印Info的所有键
813
+				t.Logf("     Info keys: %v", getMapKeys(firstMsg.Info))
814
+			} else {
815
+				t.Logf("     Info: nil")
816
+			}
817
+			if len(firstMsg.Parts) > 0 {
818
+				partsJSON, _ := json.MarshalIndent(firstMsg.Parts, "      ", "  ")
819
+				t.Logf("     Parts: %s", partsJSON)
820
+			} else {
821
+				t.Logf("     Parts: 空数组")
822
+			}
823
+		}
824
+	})
825
+}
826
+
827
+// TestMessageQueryPerformance 测试消息查询性能
828
+func TestMessageQueryPerformance(t *testing.T) {
829
+	// 外部已启动的OpenCode服务端口
830
+	externalOpenCodePort := 8787
831
+	opencodeURL := fmt.Sprintf("http://127.0.0.1:%d", externalOpenCodePort)
832
+
833
+	// 检查OpenCode服务是否运行
834
+	if !isServiceRunningForMessages(t, opencodeURL) {
835
+		t.Skipf("OpenCode服务未运行在 %s,跳过测试", opencodeURL)
836
+	}
837
+
838
+	// 检查svc-code服务是否运行
839
+	svcCodeURL := "http://localhost:8020"
840
+	if !isServiceRunningForMessages(t, svcCodeURL) {
841
+		t.Skipf("svc-code服务未运行在 %s,跳过测试", svcCodeURL)
842
+	}
843
+
844
+	t.Logf("🚀 开始测试消息查询性能")
845
+	t.Logf("OpenCode URL: %s", opencodeURL)
846
+	t.Logf("svc-code URL: %s", svcCodeURL)
847
+
848
+	// 使用现有的测试会话ID
849
+	sessionID := "ses_3aa9a94dfffeLIdWcLHCG97m7z"
850
+	t.Logf("测试会话ID: %s", sessionID)
851
+
852
+	// 先获取总消息数,以确定合适的limit值
853
+	t.Log("📊 获取消息总数...")
854
+	client, err := opencode.NewDirectClient(externalOpenCodePort)
855
+	if err != nil {
856
+		t.Fatalf("❌ 创建OpenCode客户端失败: %v", err)
857
+	}
858
+
859
+	ctx := context.Background()
860
+	allMessages, err := client.GetSessionMessages(ctx, sessionID, 0)
861
+	if err != nil {
862
+		t.Fatalf("❌ 获取总消息失败: %v", err)
863
+	}
864
+
865
+	totalMessages := len(allMessages)
866
+	t.Logf("📊 会话总消息数: %d", totalMessages)
867
+
868
+	// 定义要测试的limit值
869
+	testLimits := []int{2, 5, 10, 20, 50, 100, 200, 300}
870
+	// 如果总消息数小于某个limit,调整测试值
871
+	var adjustedLimits []int
872
+	for _, limit := range testLimits {
873
+		if limit <= totalMessages {
874
+			adjustedLimits = append(adjustedLimits, limit)
875
+		}
876
+	}
877
+	adjustedLimits = append(adjustedLimits, 0) // 0表示无限制(全量)
878
+
879
+	t.Logf("📊 测试的limit值: %v", adjustedLimits)
880
+
881
+	// 1. 测试直接OpenCode API性能
882
+	t.Run("DirectOpenCodeAPI", func(t *testing.T) {
883
+		t.Log("📊 测试直接OpenCode API性能...")
884
+
885
+		for _, limit := range adjustedLimits {
886
+			start := time.Now()
887
+
888
+			// 构造URL
889
+			url := fmt.Sprintf("%s/session/%s/message", opencodeURL, sessionID)
890
+			if limit > 0 {
891
+				url = fmt.Sprintf("%s?limit=%d", url, limit)
892
+			}
893
+
894
+			// 发送请求
895
+			client := &http.Client{Timeout: 30 * time.Second}
896
+			req, err := http.NewRequest("GET", url, nil)
897
+			if err != nil {
898
+				t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err)
899
+				continue
900
+			}
901
+			req.Header.Set("Accept", "application/json")
902
+
903
+			resp, err := client.Do(req)
904
+			if err != nil {
905
+				t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err)
906
+				continue
907
+			}
908
+
909
+			// 读取响应体(确保完全读取以测量网络传输时间)
910
+			body, err := io.ReadAll(resp.Body)
911
+			resp.Body.Close()
912
+
913
+			duration := time.Since(start)
914
+
915
+			if err != nil {
916
+				t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err)
917
+				continue
918
+			}
919
+
920
+			if resp.StatusCode != http.StatusOK {
921
+				t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d", limit, resp.StatusCode)
922
+				continue
923
+			}
924
+
925
+			// 解析消息数量(可选)
926
+			var messages []opencode.SessionMessage
927
+			if err := json.Unmarshal(body, &messages); err != nil {
928
+				t.Logf("⚠️  解析消息失败 (limit=%d): %v", limit, err)
929
+			}
930
+
931
+			actualCount := len(messages)
932
+			t.Logf("   limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节",
933
+				limit, duration, actualCount, len(body))
934
+		}
935
+	})
936
+
937
+	// 2. 测试通过svc-code API性能
938
+	t.Run("SvcCodeAPI", func(t *testing.T) {
939
+		t.Log("📊 测试svc-code API性能...")
940
+
941
+		// 用户登录获取token
942
+		token, err := loginAndGetTokenForMessagesTest(t, svcCodeURL)
943
+		if err != nil {
944
+			t.Fatalf("❌ 登录失败: %v", err)
945
+		}
946
+
947
+		for _, limit := range adjustedLimits {
948
+			start := time.Now()
949
+
950
+			// 构造URL
951
+			url := fmt.Sprintf("%s/api/session/messages", svcCodeURL)
952
+
953
+			// 构建请求体(POST方法)
954
+			requestBody := map[string]interface{}{
955
+				"sessionID": sessionID,
956
+			}
957
+			if limit > 0 {
958
+				requestBody["limit"] = limit
959
+			}
960
+
961
+			jsonBody, err := json.Marshal(requestBody)
962
+			if err != nil {
963
+				t.Errorf("❌ 编码请求体失败 (limit=%d): %v", limit, err)
964
+				continue
965
+			}
966
+
967
+			// 创建POST请求
968
+			req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
969
+			if err != nil {
970
+				t.Errorf("❌ 创建请求失败 (limit=%d): %v", limit, err)
971
+				continue
972
+			}
973
+
974
+			// 添加认证头和内容类型
975
+			req.Header.Set("Authorization", "Bearer "+token)
976
+			req.Header.Set("Content-Type", "application/json")
977
+			req.Header.Set("Accept", "application/json")
978
+
979
+			// 发送请求
980
+			client := &http.Client{Timeout: 30 * time.Second}
981
+			resp, err := client.Do(req)
982
+			if err != nil {
983
+				t.Errorf("❌ HTTP请求失败 (limit=%d): %v", limit, err)
984
+				continue
985
+			}
986
+
987
+			// 读取响应体
988
+			body, err := io.ReadAll(resp.Body)
989
+			resp.Body.Close()
990
+
991
+			duration := time.Since(start)
992
+
993
+			if err != nil {
994
+				t.Errorf("❌ 读取响应体失败 (limit=%d): %v", limit, err)
995
+				continue
996
+			}
997
+
998
+			if resp.StatusCode != http.StatusOK {
999
+				t.Errorf("❌ 请求失败 (limit=%d): 状态码 %d, 响应体: %s",
1000
+					limit, resp.StatusCode, string(body[:minForMessagesTest(200, len(body))]))
1001
+				continue
1002
+			}
1003
+
1004
+			// 解析响应
1005
+			var result struct {
1006
+				Success bool   `json:"success"`
1007
+				Message string `json:"message"`
1008
+				Data    struct {
1009
+					Messages []opencode.SessionMessage `json:"messages"`
1010
+					Count    int                       `json:"count"`
1011
+				} `json:"data"`
1012
+			}
1013
+
1014
+			if err := json.Unmarshal(body, &result); err != nil {
1015
+				t.Errorf("❌ 解析响应失败 (limit=%d): %v, 响应体: %s",
1016
+					limit, err, string(body[:minForMessagesTest(200, len(body))]))
1017
+				continue
1018
+			}
1019
+
1020
+			if !result.Success {
1021
+				t.Errorf("❌ API调用失败 (limit=%d): %s", limit, result.Message)
1022
+				continue
1023
+			}
1024
+
1025
+			actualCount := len(result.Data.Messages)
1026
+			t.Logf("   limit=%d: 耗时=%v, 返回消息数=%d, 响应体大小=%d字节",
1027
+				limit, duration, actualCount, len(body))
1028
+		}
1029
+	})
1030
+
1031
+	// 3. 性能对比总结
1032
+	t.Run("PerformanceSummary", func(t *testing.T) {
1033
+		t.Log("📊 性能对比总结:")
1034
+		t.Log("   (具体数据见上述测试输出)")
1035
+		t.Log("   📈 预期趋势:")
1036
+		t.Log("     1. limit越小,响应时间越短")
1037
+		t.Log("     2. 响应体大小与消息数量成正比")
1038
+		t.Log("     3. svc-code API会有额外开销(认证、封装)")
1039
+		t.Log("   💡 优化建议:")
1040
+		t.Log("     1. 增量加载:只查询新消息(limit=新消息数量)")
1041
+		t.Log("     2. 缓存:避免重复查询相同消息")
1042
+		t.Log("     3. 前端过滤:即使查询全量,也可缓存过滤")
1043
+	})
1044
+}

Загрузка…
Отмена
Сохранить