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()) } }) }