Files
WeiCHEN 1fc7d0d01f feat(export): 实现单次智能体回复导出、私密文档保存与隐私防泄露全套闭环 (Phase 0 - Phase 2)
- 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 自动识别为原生公式
2026-09-13 02:50:21 +00:00

188 lines
5.0 KiB
Go

package replyexport
import (
"testing"
"time"
)
func TestEvaluatePolicy_Matrix(t *testing.T) {
tests := []struct {
name string
opts PolicyOptions
wantKind ContentKind
wantPolicy ExportPolicy
wantReason ReasonCode
hasFormats bool
expectedFormat string
}{
{
name: "1. 助手已完成完整长文 -> full",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: "## 风险评估报告\n\n根据现有临床数据,该方案具有以下风险:\n1. 电气安全性需重新打标\n2. 传感器存在温漂",
},
wantKind: KindDocumentText,
wantPolicy: PolicyFull,
hasFormats: true,
},
{
name: "2. 助手已完成 + 完整正文 + 辅助卡片 -> full (仅导出正文)",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: "## 变更建议书\n\n经系统审核,以下第 3 条款存在重大缺陷,建议采纳修改意见:\n- 原文:无过温保护\n- 修改后:增加热敏电阻切断机制",
HasInteractiveCard: true,
},
wantKind: KindDocumentText,
wantPolicy: PolicyFull,
hasFormats: true,
},
{
name: "3. 助手纯交互卡片 (短引导语) -> none, CARD_ONLY",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: "请确认以下操作卡片:",
HasInteractiveCard: true,
},
wantKind: KindActionCard,
wantPolicy: PolicyNone,
wantReason: ReasonCardOnly,
hasFormats: false,
},
{
name: "4. 用户消息 -> none, NOT_ASSISTANT",
opts: PolicyOptions{
Role: "user",
Content: "请帮我分析一下项目进度",
},
wantKind: KindConversationalText,
wantPolicy: PolicyNone,
wantReason: ReasonNotAssistant,
},
{
name: "5. 系统消息 -> none, NOT_ASSISTANT",
opts: PolicyOptions{
Role: "system",
Content: "系统已建立新会话",
},
wantKind: KindConversationalText,
wantPolicy: PolicyNone,
wantReason: ReasonNotAssistant,
},
{
name: "6. 流式生成中 -> none, NOT_COMPLETED",
opts: PolicyOptions{
Role: "assistant",
Status: "streaming_status",
Content: "正在研判您的意图...",
},
wantKind: KindStatusUpdate,
wantPolicy: PolicyNone,
wantReason: ReasonNotCompleted,
},
{
name: "7. 失败/已取消消息 -> none, ERROR_OR_CANCELLED",
opts: PolicyOptions{
Role: "assistant",
Status: "failed",
HasError: true,
Content: "任务执行失败,网络超时",
},
wantKind: KindErrorNotice,
wantPolicy: PolicyNone,
wantReason: ReasonErrorOrCancelled,
},
{
name: "8. 极短寒暄 -> copy_only",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: "好的,收到!",
},
wantKind: KindConversationalText,
wantPolicy: PolicyCopyOnly,
hasFormats: true,
},
{
name: "9. 空正文 -> none, EMPTY_BODY",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: " \n\t ",
},
wantKind: KindStatusUpdate,
wantPolicy: PolicyNone,
wantReason: ReasonEmptyBody,
},
{
name: "10. 受限敏感内容 -> none, CONTENT_RESTRICTED",
opts: PolicyOptions{
Role: "assistant",
Status: "completed",
Content: "敏感内容正文",
IsRestricted: true,
},
wantKind: KindDocumentText,
wantPolicy: PolicyNone,
wantReason: ReasonContentRestricted,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kind, cap := EvaluatePolicy(tt.opts)
if kind != tt.wantKind {
t.Errorf("kind = %v, want %v", kind, tt.wantKind)
}
if cap.Policy != tt.wantPolicy {
t.Errorf("policy = %v, want %v", cap.Policy, tt.wantPolicy)
}
if tt.wantReason != "" && cap.ReasonCode != tt.wantReason {
t.Errorf("reasonCode = %v, want %v", cap.ReasonCode, tt.wantReason)
}
if tt.hasFormats && len(cap.Formats) == 0 {
t.Errorf("expected non-empty formats, got empty")
}
if !tt.hasFormats && len(cap.Formats) > 0 {
t.Errorf("expected empty formats, got %v", cap.Formats)
}
if cap.ContentSha256 == "" {
t.Errorf("expected non-empty ContentSha256")
}
})
}
}
func TestDeriveTitle(t *testing.T) {
cases := []struct {
input string
wantTitle string
}{
{
input: "# 临床评估备忘录\n正文内容...",
wantTitle: "临床评估备忘录",
},
{
input: "## 2.1 架构设计对比分析\n内容...",
wantTitle: "2.1 架构设计对比分析",
},
{
input: "针对用户提出的第三阶段审核驳回要求,项目组给出如下补救措施:\n1. 增加测试...",
wantTitle: "针对用户提出的第三阶段审核驳回要求,项目组给出如下补救措施:",
},
}
for _, tc := range cases {
got := DeriveTitle(tc.input, testTime())
if got != tc.wantTitle {
t.Errorf("DeriveTitle(%q) = %q, want %q", tc.input, got, tc.wantTitle)
}
}
}
func testTime() (t time.Time) {
return time.Date(2026, 9, 10, 15, 30, 0, 0, time.Local)
}