- Phase 0: 实现服务端回复导出策略矩阵引擎 (replyexport.EvaluatePolicy)、数据库迁移 (2026-09-11-message-export-policy.sql)、SSE done 事件挂载与能力探测 API - Phase 1: 构建前端 ReplyDocumentIR 语法树解析与渲染管道,支持 Markdown / DOCX 文件下载及带格式富文本剪贴板复制 - Phase 2: 实现私密自由文档 (freeform_documents) 领域服务、PostgreSQL 乐观锁版本管理、ACL 隐私隔离、一键共享与右侧栏编辑联动 - 专项修复: 1. 修复后台 API 路由注册并加入双向拉取同步,确保私密保存实时进入我的私密列表 2. 实现轻量回复专属 DOCX 生成与静默下载逻辑 (downloadDocxReply) 3. 将 LaTeX 公式转为标准 MathML 矢量标记,支持粘贴到 Microsoft Word / WPS 自动识别为原生公式
149 lines
5.4 KiB
Go
149 lines
5.4 KiB
Go
package server_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/server"
|
|
)
|
|
|
|
func TestServer_ReplyExportAPI(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Env: "dev",
|
|
HTTPHost: "127.0.0.1",
|
|
HTTPPort: 8893,
|
|
DataDir: t.TempDir(),
|
|
}
|
|
svc := bizdev.NewService(cfg)
|
|
srv := server.NewServer(cfg, svc)
|
|
handler := srv.Routes()
|
|
|
|
ctx := t.Context()
|
|
ctxMgr := svc.ContextManager()
|
|
sessID := "sess_export_api_test"
|
|
|
|
// 1. 注入一条完整 assistant 消息
|
|
fullContent := "# 临床风险评估报告\n\n根据系统审查,该产品存在以下风险点:\n1. 电气隔离距离不足\n2. 软件未定义看门狗"
|
|
asstID, err := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", fullContent, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("RecordUserMessageWithID failed: %v", err)
|
|
}
|
|
|
|
// 2. 注入一条纯卡片依赖引导消息
|
|
cardOnlyContent := "请确认以下操作卡片:"
|
|
cardMeta := map[string]any{
|
|
"interactive_card": map[string]any{
|
|
"card_id": "c1",
|
|
"title": "驳回门禁",
|
|
},
|
|
}
|
|
cardOnlyID, err := ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", cardOnlyContent, "", "", cardMeta)
|
|
if err != nil {
|
|
t.Fatalf("RecordUserMessageWithID cardOnly failed: %v", err)
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 A: 获取 full 消息能力
|
|
// -------------------------------------------------------------
|
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-capability", asstID), nil)
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var capRes map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &capRes); err != nil {
|
|
t.Fatalf("json unmarshal failed: %v", err)
|
|
}
|
|
if capRes["policy"] != "full" {
|
|
t.Errorf("expected policy full, got %v", capRes["policy"])
|
|
}
|
|
formats, ok := capRes["formats"].([]any)
|
|
if !ok || len(formats) == 0 {
|
|
t.Errorf("expected non-empty formats list")
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 B: 获取 cardOnly 消息能力 (policy = none, reason = CARD_ONLY)
|
|
// -------------------------------------------------------------
|
|
reqCard := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-capability", cardOnlyID), nil)
|
|
recCard := httptest.NewRecorder()
|
|
handler.ServeHTTP(recCard, reqCard)
|
|
|
|
if recCard.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d: %s", recCard.Code, recCard.Body.String())
|
|
}
|
|
var cardCapRes map[string]any
|
|
_ = json.Unmarshal(recCard.Body.Bytes(), &cardCapRes)
|
|
if cardCapRes["policy"] != "none" {
|
|
t.Errorf("expected cardOnly policy none, got %v", cardCapRes["policy"])
|
|
}
|
|
if cardCapRes["reason_code"] != "CARD_ONLY" {
|
|
t.Errorf("expected CARD_ONLY, got %v", cardCapRes["reason_code"])
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 C: 获取 full 消息正文快照
|
|
// -------------------------------------------------------------
|
|
reqSrc := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", asstID), nil)
|
|
recSrc := httptest.NewRecorder()
|
|
handler.ServeHTTP(recSrc, reqSrc)
|
|
|
|
if recSrc.Code != http.StatusOK {
|
|
t.Fatalf("expected status 200, got %d: %s", recSrc.Code, recSrc.Body.String())
|
|
}
|
|
var srcRes map[string]any
|
|
if err := json.Unmarshal(recSrc.Body.Bytes(), &srcRes); err != nil {
|
|
t.Fatalf("json unmarshal failed: %v", err)
|
|
}
|
|
if srcRes["title"] != "临床风险评估报告" {
|
|
t.Errorf("expected title '临床风险评估报告', got %v", srcRes["title"])
|
|
}
|
|
if srcRes["markdown"] != fullContent {
|
|
t.Errorf("expected markdown match, got %v", srcRes["markdown"])
|
|
}
|
|
if recSrc.Header().Get("Cache-Control") != "private, no-store" {
|
|
t.Errorf("expected Cache-Control: private, no-store, got %v", recSrc.Header().Get("Cache-Control"))
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 D: cardOnly 请求正文快照应返回 400 MESSAGE_NOT_EXPORTABLE
|
|
// -------------------------------------------------------------
|
|
reqCardSrc := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source", cardOnlyID), nil)
|
|
recCardSrc := httptest.NewRecorder()
|
|
handler.ServeHTTP(recCardSrc, reqCardSrc)
|
|
|
|
if recCardSrc.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", recCardSrc.Code)
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 E: 不存在的消息 ID 返回 404
|
|
// -------------------------------------------------------------
|
|
reqNotFound := httptest.NewRequest(http.MethodGet, "/api/v1/chat/messages/999999/export-source", nil)
|
|
recNotFound := httptest.NewRecorder()
|
|
handler.ServeHTTP(recNotFound, reqNotFound)
|
|
|
|
if recNotFound.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", recNotFound.Code)
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// 测试 F: 冲突的 content_revision 返回 409
|
|
// -------------------------------------------------------------
|
|
reqConflict := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/chat/messages/%d/export-source?content_revision=999", asstID), nil)
|
|
recConflict := httptest.NewRecorder()
|
|
handler.ServeHTTP(recConflict, reqConflict)
|
|
|
|
if recConflict.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d", recConflict.Code)
|
|
}
|
|
}
|