- 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 自动识别为原生公式
188 lines
5.0 KiB
Go
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)
|
|
}
|