Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request. Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
6.9 KiB
Go
184 lines
6.9 KiB
Go
package server_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/ctxmgr"
|
|
"chat-agent-system/internal/server"
|
|
)
|
|
|
|
func TestServer_ChatUnreadAndExportAPIs(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Env: "dev",
|
|
HTTPHost: "127.0.0.1",
|
|
HTTPPort: 8891,
|
|
DataDir: t.TempDir(),
|
|
}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := server.NewServer(cfg, svc)
|
|
handler := srv.Routes()
|
|
|
|
sessID := "sess_api_unread_export_001"
|
|
dayKey1 := "2026-09-08"
|
|
dayKey2 := "2026-09-09"
|
|
|
|
ctx := t.Context()
|
|
ctxMgr := bizdev.Snapshot(svc).Ctx
|
|
token := loginTestAdmin(t, handler, cfg)
|
|
if _, err := ctxMgr.EnsureChatSession(ctx, sessID, 1, 0, "未读导出测试"); err != nil {
|
|
t.Fatalf("EnsureChatSession failed: %v", err)
|
|
}
|
|
|
|
// 注入种子消息并获取真实自增 ID
|
|
// 消息 1: user, day 1, category=task
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息1:探讨立项需求", "TRIGGER_TASK", "", map[string]any{
|
|
"day_key": dayKey1,
|
|
})
|
|
// 消息 2: assistant, day 1, category=casual
|
|
id2, _ := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", "消息2:收到需求,开始规划。以下为初步里程碑拆解与责任分工建议,供项目组评审。", "", "", map[string]any{
|
|
"day_key": dayKey1,
|
|
"category": "casual",
|
|
})
|
|
// 消息 3: user, day 2, category=review (DOCGOV)
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息3:关于型检机构认证进度", "DOCGOV_REVIEW", "", map[string]any{
|
|
"day_key": dayKey2,
|
|
"category": "review",
|
|
})
|
|
// 消息 4: assistant, day 2, category=review — 可导出正文
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", "消息4:关于型检机构认证进度,已整理成以下要点:1) 受理号已确认;2) 补正材料清单如下;3) 预计复核周期 15 个工作日。", "DOCGOV_REVIEW", "", map[string]any{
|
|
"day_key": dayKey2,
|
|
"category": "review",
|
|
})
|
|
// 消息 5: user, day 2
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 1, "user", "消息5:另外请补充EMC整改报告", "DOCGOV_AUDIT", "", map[string]any{
|
|
"day_key": dayKey2,
|
|
"category": "review",
|
|
})
|
|
|
|
t.Run("GET /api/v1/chat/unread 统计 since_message_id 之后的未读", func(t *testing.T) {
|
|
// since_message_id=id2:当前登录用户自己的 user 消息不计入未读,仅 assistant 消息 4 未读
|
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/unread?session_id=%s&since_message_id=%d", sessID, id2), nil)
|
|
withBearerAuth(req, token)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var summary ctxmgr.ChatUnreadSummary
|
|
if err := json.NewDecoder(w.Body).Decode(&summary); err != nil {
|
|
t.Fatalf("decode unread summary failed: %v", err)
|
|
}
|
|
|
|
if summary.Total != 1 {
|
|
t.Errorf("expected total 1, got %d", summary.Total)
|
|
}
|
|
if len(summary.Items) != 1 {
|
|
t.Fatalf("expected 1 unread day bucket, got %d", len(summary.Items))
|
|
}
|
|
if summary.Items[0].Date != dayKey2 {
|
|
t.Errorf("expected date %s, got %s", dayKey2, summary.Items[0].Date)
|
|
}
|
|
if summary.Items[0].Count != 1 {
|
|
t.Errorf("expected count 1, got %d", summary.Items[0].Count)
|
|
}
|
|
if summary.NextMessageID == 0 {
|
|
t.Errorf("expected NextMessageID > 0, got %d", summary.NextMessageID)
|
|
}
|
|
|
|
// 消费最新一条未读 (POST /api/v1/chat/read)
|
|
readBody := fmt.Sprintf(`{"session_id":"%s","user_id":0,"read_message_ids":[%d]}`, sessID, summary.NextMessageID)
|
|
readReq := httptest.NewRequest(http.MethodPost, "/api/v1/chat/read", strings.NewReader(readBody))
|
|
readReq.Header.Set("Content-Type", "application/json")
|
|
withBearerAuth(readReq, token)
|
|
readW := httptest.NewRecorder()
|
|
handler.ServeHTTP(readW, readReq)
|
|
|
|
if readW.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK on /api/v1/chat/read, got %d: %s", readW.Code, readW.Body.String())
|
|
}
|
|
|
|
// 再次获取未读,数量应减为 1
|
|
req2 := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/unread?session_id=%s&since_message_id=%d", sessID, id2), nil)
|
|
withBearerAuth(req2, token)
|
|
w2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(w2, req2)
|
|
|
|
var summary2 ctxmgr.ChatUnreadSummary
|
|
_ = json.NewDecoder(w2.Body).Decode(&summary2)
|
|
if summary2.Total != 0 {
|
|
t.Errorf("expected total 0 after consuming the assistant unread, got %d", summary2.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("GET /api/v1/chat/export/messages 导出全量并支持分类过滤", func(t *testing.T) {
|
|
// 1. 无过滤条件导出
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID, nil)
|
|
withBearerAuth(req, token)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var msgs []ctxmgr.ChatMessageRecord
|
|
if err := json.NewDecoder(w.Body).Decode(&msgs); err != nil {
|
|
t.Fatalf("decode export messages failed: %v", err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("expected 2 exportable assistant messages, got %d", len(msgs))
|
|
}
|
|
|
|
// 2. 按分类 review 过滤
|
|
reqReview := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID+"&category=review", nil)
|
|
withBearerAuth(reqReview, token)
|
|
wReview := httptest.NewRecorder()
|
|
handler.ServeHTTP(wReview, reqReview)
|
|
|
|
if wReview.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d: %s", wReview.Code, wReview.Body.String())
|
|
}
|
|
var reviewMsgs []ctxmgr.ChatMessageRecord
|
|
if err := json.NewDecoder(wReview.Body).Decode(&reviewMsgs); err != nil {
|
|
t.Fatalf("decode filtered export failed: %v", err)
|
|
}
|
|
if len(reviewMsgs) != 1 {
|
|
t.Fatalf("expected 1 exportable review assistant message, got %d", len(reviewMsgs))
|
|
}
|
|
|
|
// 3. 按日期过滤
|
|
reqDate := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID+"&date_from=2026-09-09&date_to=2026-09-09", nil)
|
|
withBearerAuth(reqDate, token)
|
|
wDate := httptest.NewRecorder()
|
|
handler.ServeHTTP(wDate, reqDate)
|
|
if wDate.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 OK, got %d: %s", wDate.Code, wDate.Body.String())
|
|
}
|
|
var dateMsgs []ctxmgr.ChatMessageRecord
|
|
if err := json.NewDecoder(wDate.Body).Decode(&dateMsgs); err != nil {
|
|
t.Fatalf("decode date-filtered export failed: %v", err)
|
|
}
|
|
if len(dateMsgs) != 1 {
|
|
t.Fatalf("expected 1 exportable message on 2026-09-09, got %d", len(dateMsgs))
|
|
}
|
|
|
|
// 4. 跨用户导出应被拒绝
|
|
reqIDOR := httptest.NewRequest(http.MethodGet, "/api/v1/chat/export/messages?session_id="+sessID, nil)
|
|
withUserIDAuth(reqIDOR, loginTestUser(t, handler, "zhanggong", "123456"))
|
|
wIDOR := httptest.NewRecorder()
|
|
handler.ServeHTTP(wIDOR, reqIDOR)
|
|
if wIDOR.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for cross-user session export, got %d: %s", wIDOR.Code, wIDOR.Body.String())
|
|
}
|
|
})
|
|
}
|