build-flow.api/docs/superpowers/specs/2026-07-07-sop-test-unit-factory-design.md
xiangning 16ba77f69f feat(platform): 实现权限版本控制即时失效旧token机制
- 在JWT中添加permver权限版本号claim,用于实时权限变更检测
- 实现PermissionVersionMiddleware中间件,在认证后授权前校验权限版本
- 修改AssignPermissionsCommandHandler,角色权限变更时自增相关用户权限版本号
- 修改AssignRolesCommandHandler,用户角色变更时自增权限版本号强制重登录
- 集成Redis存储权限版本镜像,支持分布式环境下的版本同步
- 添加PermissionVersion字段到User实体,用于存储权限版本号
- 实现分布式登录失败计数与账号锁定功能,增强安全防护
- 配置文件中新增AccessTokenMinutes和RefreshTokenDays可配置项
- 更新CORS策略配置,添加127.0.0.1本地访问支持
- 完善异常处理,统一使用ErrorCode枚举进行错误码管理
- 添加SOP测试单元设计文档,规范端到端测试流程
2026-07-07 17:18:05 +08:00

18 KiB
Raw Blame History

SOP 测试单元工厂设计 · TDD + SDD 三驱动融合,推送飞书

  • 状态:已批准(Approved)
  • 日期:2026-07-07
  • 作者:brainstorming session
  • 影响范围:全 6 后端模块(POC 阶段聚焦 rag);产出物推送至飞书知识库 7659208807054117857 的"产品"目录

一、目标与背景

把 BuildFlow"现在的功能"拆解为可执行、可追踪、可复制的最小 SOP 测试单元,通过三位一体的工作流落地到飞书(文档 + 任务联动),实现端到端场景的标准化验证。

三驱动定义(与用户对齐)

驱动 全称 在本项目中的具体含义
TDD-文档 Technical Design Document 技术设计文档:架构、接口契约、数据模型、时序图、边界条件
TDD-测试 Test-Driven Development 测试驱动开发:先写 xUnit 测试(红)→ 实现(绿)→ 重构
SDD Skill-Driven Development 技能驱动开发:复用项目已有的 .agents/skills/rag-coding-api 编码规范(MediatR CQRS、测试规范、模块边界)

现状(已通过代码探索确认)

维度 现状 与本设计的关系
测试体系 8 个 xUnit 测试项目,1472 源文件 vs 92 测试文件,FluentAssertions + EF InMemory 成熟,但全部是单元/Handler 级,无端到端 SOP 测试
RAG 测试 BuildFlow.Rag.Tests 仅 7 个文件,覆盖 sanitizer/parser/catalog/validator 关键缺口:无 ProcessDocumentCommandHandler、SendMessageCommandHandler、RagQueryCommandHandler、RAGStreamEndpoint 的集成测试
文档 仅 scripting/x6-workflow/CI-CD 三份设计文档,散落各子工程 无统一 PRD/SDD/SOP 体系
技能规范 .agents/skills/rag-coding-api(27KB)已强制 MediatR CQRS、禁 service bridge、测试规范 SDD 的载体已就绪,本设计直接复用
飞书 已有知识库 7659208807054117857,含"服务器配置""项目开发指南"两个根节点 本设计在其下新建"产品"目录承载 SOP 产出

二、核心决策(已与用户确认)

# 决策点 选择 理由
1 功能范围 全部 6 后端模块 rag/file/im/platform/workflow/scripting 全覆盖
2 推进策略 POC 先行:RAG 跑通再复制 风险低、模式可复制、工作量可控
3 测试单元粒度 端到端场景 用户视角完整流程(非原子用例)
4 飞书产出形式 飞书文档 + 飞书任务联动 文档沉淀设计,任务追踪执行
5 RAG 场景 A=知识库 RAG 问答全链路 + B=多轮对话流式问答 互补覆盖 RAG 两条主干(检索 vs 对话)
6 pgvector 隔离 Stub 先行 + Testcontainers 集成测试补充 POC 用 Stub 快速跑通,后续补真实召回验证
7 实现顺序 文档先行 → 测试后补 文档引导实现,飞书文档作为单一事实来源
8 模板化 8 段式文档骨架 + 5 任务结构 Phase 2 可填模板复制到其余 5 模块

三、SOP 测试单元工厂架构

3.1 生产流水线

[SDD 规范]              [TDD 设计]              [TDD 实现]
rag-coding-api skill →  技术设计文档        →    xUnit 测试先行
(MediatR CQRS /         (架构/接口/数据模型/      (红色测试 →
 测试规范)                时序/边界)               绿色实现 → 重构)
       │                      │                       │
       └──────────────────────┴───────────────────────┘
                              │
                              ▼
                 ┌─────────────────────────┐
                 │   最小 SOP 测试单元       │
                 │   (端到端场景)            │
                 └────────────┬────────────┘
                              │
                ┌─────────────┴─────────────┐
                ▼                           ▼
       ┌─────────────────┐         ┌────────────────────┐
       │  飞书文档         │         │  飞书任务           │
       │  (SOP + 设计)    │◄──────►│  (检查项可勾选)     │
       │  评审/沉淀         │  deep   │  执行/追踪           │
       │                  │  link   │                    │
       └─────────────────┘         └────────────────────┘

3.2 飞书知识库目录结构

📚 Wiki 空间 7659208807054117857
├── 📄 服务器配置 (已存在)
├── 📄 项目开发指南 (已存在)
└── 📁 产品/                         ← 已创建 node_token=StjYwzzsSisql1ki2oqc5KFEnZd
    ├── 📄 产品概览 - BuildFlow 模块地图
    └── 📁 SOP 测试单元/
        ├── 📁 RAG 模块/
        │   ├── 📄 [SOP-RAG-01] 知识库 RAG 问答全链路
        │   └── 📄 [SOP-RAG-02] 多轮对话流式问答
        ├── 📁 File 模块/      (Phase 2)
        ├── 📁 IM 模块/         (Phase 2)
        ├── 📁 Platform 模块/   (Phase 2)
        ├── 📁 Workflow 模块/   (Phase 2)
        └── 📁 Scripting 模块/  (Phase 2)

四、SOP 测试单元标准模板

每个 SOP 测试单元(无论 RAG 还是后续模块)都遵循同一个 8 段式文档骨架 + 5 任务结构。这是 Phase 2 能快速复制的关键。

4.1 飞书文档骨架(8 段式)

# [SOP-XX] 场景名称
## 模块: rag | 场景类型: 端到端 | 状态: 草稿/评审/通过

## 1. 用户故事与价值
   作为...我希望...以便...

## 2. 前置条件 (SDD 规范约束)
   - 遵守 rag-coding-api skill: MediatR CQRS / 测试规范
   - 依赖的外部服务及隔离方式 (Stub/Testcontainer)

## 3. 技术设计 (TDD 文档部分)
   3.1 架构图 (mermaid 时序图)
   3.2 涉及的 Command/Query/Handler 清单
   3.3 数据模型变更 (实体/字段)
   3.4 关键接口契约 (输入/输出)

## 4. SOP 执行步骤 (端到端)
   Step 1...N: 每步含 API 调用 / Handler / 预期结果

## 5. 验收标准 (Definition of Done)
   ☐ 可勾选的检查项列表

## 6. 测试设计 (TDD 测试部分)
   6.1 测试文件位置与命名
   6.2 Stub/Fixture 设计
   6.3 测试用例清单 (Given/When/Then)

## 7. 执行结果记录
   - 测试运行命令与输出
   - 覆盖率
   - 已知限制

## 8. 变更日志

4.2 飞书任务结构(每个 SOP 单元 = 1 父任务 + 5 子任务)

父任务: [SOP-XX] 场景名称 - 端到端验证
├── ☐ T1: 编写 TDD 技术设计文档 (description 含飞书文档 deep link)
├── ☐ T2: 编写 xUnit 测试用例 (红色阶段)
├── ☐ T3: 实现/对齐 Handler 逻辑 (绿色阶段)
├── ☐ T4: 跑通端到端测试,记录结果 (链接到文档§7)
└── ☐ T5: 评审 + 标记场景状态为「通过」

任务描述嵌入飞书文档 deep link,实现文档 ↔ 任务双向联动。


五、RAG 模块 POC 场景详细设计

5.1 场景 A:[SOP-RAG-01] 知识库 RAG 问答全链路

用户故事:作为知识工作者,我上传一份文档到知识库,提问后能获得基于该文档的准确回答和引用来源。

覆盖链路:建 KB → 上传文档 → 触发处理流水线(提取/分块/向量化/入库)→ RAG 检索问答。

SOP 步骤(端到端):

Step API 调用 MediatR 预期结果
1 POST /knowledge-bases CreateKnowledgeBaseCommand KB 创建,配 RetrievalMode=Hybrid/TopK=5/阈值=0.7
2 POST /knowledge-bases/{id}/documents (multipart) UploadDocumentCommand Document 落库 Status=Pending
3 POST /documents/{id}/process ProcessDocumentCommand 4 阶段流水线:提取→分块→向量化→入库,Status=Processed
4 POST /rag/query RagQueryCommandHandler 返回 {answer, sources[]},sources 非空

涉及 Handler:CreateKnowledgeBaseCommandHandler、UploadDocumentCommandHandler、ProcessDocumentCommandHandler、RagQueryCommandHandler、IRagRetrievalService、IAIChatAgent。

验收标准(DoD):

  • ☐ 文档 Status 从 Pending → Processing → Processed
  • ☐ DocumentChunk 数量 = 预期分块数,Content 非空
  • ☐ /rag/query 返回 sources[] 非空且 DocumentId 匹配
  • ☐ answer 包含 LLM 响应文本
  • ☐ 全程无未捕获异常

测试隔离策略:

外部依赖 处理方式
pgvector POC:Stub IRagRetrievalService 返回预设 sources;补充:Testcontainers + 真实 PG+pgvector 集成测试
LM Studio Stub IAIChatAgent(RunAsync 返回固定文本)+ Stub IEmbeddingService(返回固定 768 维向量)
对象存储 Stub IFileStorageClient(复用 StubFileStorageClient 模式)
文档提取 Stub IDocumentExtractorFactory + IDocumentExtractor(返回固定文本)

测试文件:tests/BuildFlow.Rag.Tests/RagQAEndToEndTests.cs


5.1.1 分块策略详解(ChunkingMode 6 种模式)

分块在 ProcessDocumentCommand 阶段 2,由 ITextChunker.Chunk(extracted, mode, size, overlap, separator) 完成(实现 Infrastructure/AI/TextChunker.cs,设计参考 Dify):

模式 值 切分逻辑
General 0(默认) 段落感知:先按 \n\n 切段,超长段按句子标点二切,overlap 取最后 1-2 句
Heading 1 按 Markdown H1-H6 切,维护 headingStack,生成 HeadingPath 如 "第三章 > 3.1 概述"
ParentChild 2 父块=整段(上下文,最大 chunkSize),子块=段落再切(chunkSize/3,精确匹配),父子都向量化
Separator 3 按自定义分隔符(默认 \n\n---\n\n)切分,超长用 General 二切
ByPage 4 PDF 每页/PPT 每张一块,页过长用 General 二切,子块共享 PageNumber
Token 5 按 token 估算:charBudget = chunkSize × 1.5(注释称"中文=2/英文=1"但实际用固定 1.5 系数,存在实现差异)

ParentChild 回填机制:先 SaveChanges 拿父块 Guid → 用 parentIdMap[Index→Guid] 反查 → 写入子块 ParentChunkId(两轮 SaveChanges)。

参数优先级:模式 doc.ChunkingModeOverride ?? kb.ChunkingMode(文档级覆盖优先);ChunkSize/ChunkOverlap/Separator 全部来自 kb,默认 500/50/null。

5.1.2 向量化策略详解

  • 接口:IEmbeddingService.EmbedAsync(单条) + EmbedBatchAsync(批量),实现 Infrastructure/AI/EmbeddingService.cs,OpenAI 兼容协议
  • 批处理:EmbeddingBatchSize = 64,≤64 一次调用,否则按 64 步长切片按原顺序拼接(顺序必须与输入一致)
  • 向量维度 = 768(非 384/1536):默认模型 embeddinggemma-300m 输出 768 维,DB 列 vector(768),换模型必须同步改迁移
  • 写入方式:EF 忽略 Embedding 列(DocumentChunkConfiguration.Ignore),逐块 raw SQL UPDATE document_chunks SET embedding='[...]'::vector WHERE "Id"=...(N 条 SQL,无批量优化)
  • TokenCount 粗估:Content.Length / 4,非真实 tokenizer

5.1.3 召回与重排算法详解

IRagRetrievalService.RetrieveAsync(kb, question) → List<RetrievalHit>,4 步流水线:

  1. 粗召回 RecallAsync:SQL LIMIT RetrievalTopK(默认 20)
  2. 阈值过滤:VectorScore >= SimilarityThreshold(默认 0.3),纯 Keyword 跳过
  3. 内存重排:按 mode 计算 finalScore,Take(ContextTopK)(默认 5)
  4. 父块回填 EnrichWithContextAsync(当前空壳,见差异清单)

Vector 召回 SQL(pgvector 余弦距离):

SELECT ..., 1 - (dc.embedding <=> '{q}'::vector) AS similarity
FROM document_chunks dc JOIN documents d ON ...
WHERE d."KnowledgeBaseId" = '{kbId}' AND dc.embedding IS NOT NULL
  AND dc."ParentChunkId" IS NULL       -- 只召回父块/独立块
ORDER BY dc.embedding <=> '{q}'::vector LIMIT 20

Keyword 召回:ExtractKeywords 分词(英文 [a-zA-Z]+≥2,中文 ≤4 整体/>4 做 2-4 字 N-gram),每个关键词 CASE WHEN Content ILIKE @kwN THEN 1 ELSE 0 相加得命中数。

Hybrid 重排加权:finalScore = VectorScore × VectorWeight + keywordScore × (1-VectorWeight),默认 VectorWeight=0.7(70% 向量 + 30% 关键词)。

5.1.4 设计意图 vs 当前实现差异清单(技术债务)

代码探索发现 4 处差异,POC 用 Stub 会绕过,但 Testcontainers 集成测试必须覆盖:

# 差异点 设计意图 当前实现
1 RerankStrategy 未读取 枚举 None/Keyword/LLM 可切换重排 重排分支依据是 RetrievalMode,该字段从未被读取,LLM 重排未实现
2 Hybrid 非双路融合 向量召回 + 关键词召回融合(RRF/加权) Hybrid 只执行向量召回 SQL,关键词仅在内存重排加权参与,未做关键词路独立召回
3 父块回填空壳 召回子块→回填父块获完整上下文 EnrichWithContextAsync 无 DB 查询,ContextContent=Content;SQL ParentChunkId IS NULL 召回的是父块而非子块
4 表名不一致(潜在 bug) EF 表 b_document_chunks raw SQL 写 document_chunks(无 b_ 前缀),可能导致向量写入失败

5.2 场景 B:[SOP-RAG-02] 多轮对话流式问答

用户故事:作为用户,我与 AI 进行多轮对话,AI 能记住上下文并以流式方式逐字返回。

覆盖链路:建会话 → 流式提问 → 多轮历史拼装 → SSE 协议 → 缓存一致性。

SOP 步骤(端到端):

Step API 调用 处理路径 预期结果
1 POST /chat/conversations CreateConversationCommand 会话创建(不绑 KB)
2 POST /chat/conversations/{id}/stream StreamMessageEndpoint(直连) SSE token 流,event:done 返回 messageId
3 POST /chat/conversations/{id}/stream(第二轮) 同上 历史拼装含第一轮 user+assistant
4 校验 IChatMessageCache ↔ DB cache 与 DB ChatMessage 一致

涉及 Handler/Endpoint:CreateConversationCommandHandler、StreamMessageEndpoint、IAIChatAgent.RunStreamingAsync、IChatMessageCache、AiChatOutputSanitizer。

验收标准(DoD):

  • ☐ 两轮 SSE 均以 event:done 结束并返回 messageId
  • ☐ 第二轮请求时,历史拼装包含第一轮的 user+assistant 消息
  • ☐ AiChatOutputSanitizer 生效(去开场白/think 标签)
  • ☐ Redis cache 与 DB 消息记录一致
  • ☐ 会话可删除(DELETE 返回成功)

测试隔离策略:

外部依赖 处理方式
LM Studio Stub IAIChatAgent.RunStreamingAsync(yield 固定 token 序列)
Redis Stub IChatMessageCache(内存字典模拟)或用真实 Redis

测试文件:tests/BuildFlow.Rag.Tests/ChatStreamEndToEndTests.cs


六、Stub/Fixture 设计(共享测试基础设施)

复用并扩展现有 PreviewDocumentChunksQueryHandlerTests 的 Stub 模式,新增以下共享测试基础设施(放 tests/BuildFlow.Rag.Tests/Stub/):

Stub/Fixture 实现接口 用途
StubFileStorageClient IFileStorageClient 提供文件流(内存 Stream),不调真实对象存储
StubDocumentExtractor IDocumentExtractor 返回固定提取文本
StubDocumentExtractorFactory IDocumentExtractorFactory 按扩展名分发 Stub
StubEmbeddingService IEmbeddingService 返回固定维度向量(如 384 维)
StubAIChatAgent IAIChatAgent RunAsync 返回固定文本;RunStreamingAsync yield 固定 token
StubRagRetrievalService IRagRetrievalService 返回预设 sources 列表(避开 pgvector)
StubChatMessageCache IChatMessageCache 内存字典模拟 Redis cache

测试 Host 搭建模式:参考 BuildFlow.IM.Integration.Tests 的 ImServerFixture/ImServerCollection 模式,搭建 RagEndToEndFixture(注册 Stub 到 DI 容器 + EF InMemory + WebApplicationFactory)。


七、Phase 划分与里程碑

Phase 1:RAG POC(本次实现)

里程碑 交付物 验收
M1: 飞书落地 产品目录 + 产品概览 + RAG 2 份 SOP 文档 + 对应飞书任务 文档可访问,任务可勾选
M2: 测试实现 2 个 E2E 测试文件 + 7 个 Stub 类 + RagEndToEndFixture dotnet test 全绿
M3: POC 验收 测试输出回填飞书文档§7,场景状态标记「通过」 模板固化,可复制

Phase 2:复制到其余 5 模块(后续)

按 file → im → platform → workflow → scripting 顺序,每模块选 1-2 个端到端场景,套用 Phase 1 固化的模板。


八、风险与缓解

风险 缓解
pgvector 无法 InMemory 测试 POC 用 Stub RetrievalService;补充 Testcontainers 集成测试(需 Docker)
SSE 流式测试断言复杂 用 HttpContent.ReadAsStreamAsync 逐行解析 SSE,参考现有 OpenAiCompatibleChatStreamParserTests
StreamMessageEndpoint 直连不经 MediatR 测试用 WebApplicationFactory 走真实 HTTP,而非直接调 Endpoint
Phase 2 模块差异大 8 段式模板的可变部分集中在§3 技术设计,其余段落复用

九、未来扩展(YAGNI - 本次不做)

  • Web 前端端到端 SOP(Playwright,现有体系已支持)
  • Portal/Client 端 SOP
  • 飞书任务自动同步测试结果(CI/CD webhook)
  • SOP 执行自动化(CLI 一键跑全部场景)

附录 A:关键文件索引

路径 作用
build-flow.api/src/modules/rag/Application/ Command/Query/Handler 全清单
build-flow.api/src/modules/rag/Domain/Entities/ KnowledgeBase/Document/DocumentChunk/Conversation/ChatMessage
build-flow.api/src/modules/rag/Infrastructure/AI/RagRetrievalService.cs 向量检索核心
build-flow.api/src/BuildFlow.Host/Endpoints/Rag/ HTTP 路由
build-flow.api/tests/BuildFlow.Rag.Tests/ 现有测试 + 本次新增 E2E
.agents/skills/rag-coding-api/SKILL.md SDD 规范来源
飞书 Wiki 7659208807054117857 / 产品节点 StjYwzzsSisql1ki2oqc5KFEnZd SOP 产出落点