Files

35 KiB
Raw Permalink Blame History

私有资料问答(FR-PRIV)测试用例表 · TEST_CASES_PRIVATE.md

docs/team/TEST_CASES_PRIVATE.md | T1(测试 agent)编制,TL 复查达标后方可进入测试执行。 权威规格:docs/PRD.md §17(FR-PRIV-001~008)、docs/UI_SPEC.md §9、docs/PRIVATE_DOCS_TEST_PLAN.md。 实现契约:docs/team/brief_priv_a1.md(A1 任务书,含全部签名/常量/JSON 契约)。 基线:feature/bioflow-conversation-orchestration @ dea4640(编制时)。 修订记录:v1.1(TL 复查裁决)——假设 1 替换为实施方案 §1.2 语义(表格原位聚合、outline 不参与切块);假设 9 改行为级判据;TC-PRIV-L1-05/L1-07 修订并新增 L1-07b(outline 不影响输出)、L1-42(跨轮归属)。用例总数 90 → 92,追溯矩阵已同步。 修订记录:v1.2(R2 回写)——TC-PRIV-L2-10/L2-11 信封含 ok:true;TC-PRIV-L2-12 确认空 question 与缺 question 均 422;TC-PRIV-L3-01/L4-03 注明无关查询 → hit:false(自谦答语确定性改判);TC-PRIV-L2-10 注明 BUG-PRIV-R2-01(LLM 上下文不含 question)现状。 修订记录:v1.3(R3 回写)——TC-PRIV-L2-10 判据去掉 R2 注,改为「LLM 收到用户问题与参考资料」(BUG-PRIV-R2-01 已关闭,92e2754)。

0. 约定

  • 编号:TC-PRIV-{L层}-{序号},L层 ∈ L1/L2/L2s/L3/L4。
  • 判据:每条含可验证判据(PASS 条件);与 A1 任务书契约对齐。
  • 假设(编制时 A1 未交付,按任务书契约推断,TL 复查裁决):
    1. 【TL 裁决,以实施方案 §1.2 为准】build_chunks 语义:表格矩阵化段(|a|b| 行式)插入段落流原位参与聚合切块(废除「末尾 table 单独块」);outline 不参与 chunk 切块(chunk 正文保持纯净,标题上下文仅由首块前缀 # {title} 提供;签名保留 outline 参数,A1 实现为忽略或仅取用 title)。
    2. 长段落(> TARGET_CHARS)整段不切,独占一块(单块可超 600 字);空正文(paragraphs/tables 全空,outline 不参与切块)产出 0 块且不报错。
    3. ChunkIndex.load() 对 index.json / texts.jsonl / index.faiss 三文件中任一缺失或损坏:按空库降级(可服务,不抛异常);rebuild_all() 覆盖三文件(tmp+os.replace 原子切换)。
    4. ChunkIndex 返回的 snippet 为 text[:200]。
    5. embed_batch_chunked 批量失败按「批」重试:整批重试一次,仍失败该批全部降级零向量;并发上限 = workers=4(线程数)。
    6. search-chunks 无降级档:低于 min_score 全部丢弃 → mode:"empty", items:[](空库同)。
    7. rebuild-chunks 期间 /health、/search-chunks 可服务:由「重建不使用 HTTP(CLI 子进程)」保证(L2s 用例验证)。
    8. /api/qa/private 三态:命中 200 / 未命中 200 / vstore 不可达 503 / LLM 失败 502;未命中与错误信封前端一律静默。
    9. 【TL 裁决,以实施方案 §3.2 为准】前端 _firePrivate 本轮上下文校验实现 = streamCtx[sessionId].aMsg 引用相等;用例判据写行为级(「非本轮私有结果不得串挂」),不锁内部实现。_flushPrivate 只清 _privatePending(A1 任务书已批准设计)。
    10. removeLibrary(id) 在 http.js del 基础上自行处理 204 空体并返回 {ok:true}。
  • 测试环境:vstore pytest 用 vstore/tests/conftest.py(固定 Fernet key + 假 embed);BFF 用 MockTransport 注入 client / monkeypatch 服务函数;前端用 vitest(jsdom)+ vi.mock;L2/L2s/L4 用 curl / 浏览器(TL 部署后执行)。
  • 纪律:本文件与测试骨架之外不改任何产品代码;不 commit、不部署。

1. 分层用例

L1 单元测试(vstore chunks / embed / vault、BFF privateqa / schemas、前端 store 逻辑)

L1-chunks(build_chunks 切块与 ChunkIndex)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L1-01 FR-PRIV-002 vstore/chunks.py build_chunks tmp_path;模块导入 构造 8 段短段落(每段 < 600 字)直接调用 title="报告", outline=[], paragraphs=[p1..p8 短段], tables=[] 恰好 1 块;text 首行 = "# 报告";块内含全部 8 段 单块达到 8 段上限(MAX_PARAS)即封块;len(chunks)==1;chunks[0].seq==0
TC-PRIV-L1-02 FR-PRIV-002 build_chunks 字符封块 tmp_path 段落总长 ≥ 600 字(如 7 段×90 字) title="T", paragraphs=7 段×90 字 ≥1 块;先满 600 字即封块(不要求满 8 段) 存在块 text 长度 ≥ 600;块数 > 1 时相邻块正确切分
TC-PRIV-L1-03 FR-PRIV-002 build_chunks 重叠段 tmp_path 构造 12 段 → 应产出 2 块 title="T", paragraphs=p1..p12 块2 首段 == 块1 末段(重叠 1 段) chunks[1].text 以 chunks[0] 末段开头;无重复段顺序错误
TC-PRIV-L1-04 FR-PRIV-002 build_chunks 200 块上限 tmp_path 构造 > 200×600 字的超长输入 300 段×700 字 恰好 200 块;seq 0..199 len(chunks)==200;MAX_PER_DOC 截断生效;不抛异常
TC-PRIV-L1-05 FR-PRIV-002 build_chunks 表格原位矩阵化聚合(TL 裁决版) tmp_path;段落流中夹表格(与实施方案 §1.2 对齐) 构造段落-表格-段落混合流,直接调用 title="T", paragraphs=["前置段落"*N, "后续段落"*M], tables=[2×2 表](表格位于段落流中间) 表格矩阵化为 |a|b| 行式文本段,按原位参与聚合切块;输出块序反映原始位置(表格段在相邻段落之间) 表格行式文本出现在块中且前后位置与输入流一致;废除「末尾 table 单独块」
TC-PRIV-L1-06 FR-PRIV-002 build_chunks 首块标题前缀 tmp_path 任意 1 段正文 title="小麦试验", paragraphs=[p] 首块 text 以 "# 小麦试验" 开头 chunks[0].text.startswith("# 小麦试验")
TC-PRIV-L1-07 FR-PRIV-002 build_chunks 空输入 tmp_path 全空输入(outline 不参与切块) title="", paragraphs=[], outline=[], tables=[] 返回 [](不抛异常) len(chunks)==0;chunk 失败不阻断 doc 级(配合 TC-PRIV-L1-19)
TC-PRIV-L1-07b FR-PRIV-002 build_chunks outline 不参与切块(TL 裁决版) tmp_path;实施方案 §1.2 输入相同 paragraphs + 两组不同 outline([A1,A2] 与空 []) title="T", paragraphs=p1..p3, outline 两版对比 两次调用产出的 chunk 集合逐块 text 完全一致(outline 不影响 chunk 输出) 两版输出逐块相等;chunk 正文中不含 outline 文本
TC-PRIV-L1-08 FR-PRIV-002 ChunkIndex upsert/查询 tmp_path;假 embed 新建 ChunkIndex → upsert_document(doc_id, items) → rows()/count() 3 块 doc"A" count()==3;rows 含 chunk_id/seq/snippet/doc_name chunk_id == "A:0"…"A:2";字段齐备
TC-PRIV-L1-09 FR-PRIV-002 ChunkIndex upsert 替换语义 tmp_path 先 upsert A(3 块) 再 upsert A(2 块) 同 doc_id 二次 upsert count()==2;旧 3 行全部消失(替换非追加) A 行数 ==2;旧 seq 行不存在
TC-PRIV-L1-10 FR-PRIV-002 ChunkIndex remove_document tmp_path upsert A+B 后 remove("A") doc_ids=["A"] count()==B 块数;A 行全消失;不存在返回 False remove 返回 True;再 remove("A") 返回 False
TC-PRIV-L1-11 FR-PRIV-002 ChunkIndex 原子写 tmp_path upsert 后检查三文件;再造 ChunkIndex(path).load() 重载 3 块 index.json/texts.jsonl/index.faiss 三文件存在;重载后 count()==3 重载行与写入一致;目录内无 .tmp 残留(tmp+os.replace)
TC-PRIV-L1-12 FR-PRIV-002 ChunkIndex 阈值/top_k/doc_ids/exclude_template tmp_path;可控分数向量 造高分/低分两组向量行 search(q_vec, top_k=2, min_score=0.30, doc_ids=[...], exclude_template=True) 仅 ≥0.30 命中;≤2 条;doc_ids 白名单过滤生效;as_template 行被排除 命中集合、条数、过滤三判据同时满足
TC-PRIV-L1-13 FR-PRIV-002 ChunkIndex 低于阈值→empty tmp_path 查询向量与所有行内积 < 0.30 search(..., min_score=0.30) mode=="empty", items==[](无降级档) mode 为 "empty" 且 items 为空

L1-embed(批量嵌入)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L1-14 FR-PRIV-002 vstore/embed.py embed_batch_chunked monkeypatch 注入假请求钩子(记 batch 调用与输入形态) 注入 _request_batch(返回假向量);调 embed_batch_chunked 40 条文本, batch_size=16, workers=4 3 批调用;每批 payload 的 input 是数组(非单字符串);返回 40 个 1024 维向量且顺序保持 批次数 == ceil(40/16)==3;每批 input 为 list;len(results)==40;顺序可验证
TC-PRIV-L1-15 FR-PRIV-002 embed_batch_chunked 并发上限 monkeypatch 记录并发峰值 钩子里计数器模拟执行耗时 33 条文本, workers=4 并发峰值 ≤4;结果为 3 批 计数器峰值 ==4(或 ≤4)
TC-PRIV-L1-16 FR-PRIV-002 embed_batch_chunked 单批重试 monkeypatch:首调抛错、二调成功 第 2 批首调失败 20 条文本 该批重试 1 次成功;总批次数 2+1(重试)+1;结果全有向量 重试计数 ==1;该批向量非零向量
TC-PRIV-L1-17 FR-PRIV-002 embed_batch_chunked 降级零向量 monkeypatch:某批两次全失败 第 1 批两次失败 20 条文本 失败批全部零向量;其余批正常;函数不抛 results[0:batch] 全 [0.0]*1024;其余非零;不抛异常

L1-vault(钩子)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L1-18 FR-PRIV-002 vault.py add() chunk 钩子成功路径 monkeypatch:chunks.embed_batch_chunked 抛 EmbedError(或 build_chunks 被 patch 抛) vault.add() 正常入库 含正文的 txt doc 级正常返回;返回体含 chunk_count==chunks 数、chunk_ok==True、chunk_error=="" 返回体三字段存在且 chunk_ok 为 True
TC-PRIV-L1-19 FR-PRIV-002 vault.py chunk 失败不阻断 doc 级 monkeypatch embed_batch_chunked 抛异常 vault.add() 含正文的 txt doc 级照常入库(document_id/enc 存在);chunk_ok==False;chunk_count==0 或实际数;chunk_error 非空 doc 级成功 + chunk_ok==False + chunk_error 非空
TC-PRIV-L1-20 FR-PRIV-006 vault.py delete() chunk 联动删除 先 add 产生 chunk 行 vault.delete(doc_id) doc_id 主索引删除成功;chunk 索引该 doc 行消失 chunks.count() 回到删除前数量;doc 级 removed==True
TC-PRIV-L1-21 FR-PRIV-007 vault.py rebuild_chunks() 先 add 2 条;chunks.clear() 后调 rebuild_chunks rebuild_chunks() 无 返回 {docs, chunks, duration_ms};chunk 行重建(≥1) docs==2;chunks>0;数字字段类型正确

L1-BFF(privateqa / schemas / llm)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L1-22 FR-PRIV-004 services/privateqa.py ask_private 命中链路 monkeypatch search_chunks 返回 2 doc 4 块;complete_text 返回带 [资料n] 文本 await ask_private("问") question hit==True;answer 非空;items 带 ref;elapsed_ms 数字 items[].ref 为编号(1..n);answer 含 [资料1]
TC-PRIV-L1-23 FR-PRIV-004 ask_private 空结果 monkeypatch search_chunks 返回 {ok:true,items:[]} await ask_private question {hit:false, mode:"empty", answer:"", items:[]};不调 LLM 返回体一致且 complete_text 未被调用
TC-PRIV-L1-24 FR-PRIV-004 ask_private 每 doc≤3 去重 5 块同 doc + 2 块另一 doc;top_k=8 await ask_private question items 每 doc ≤3 块;同 doc 按 score 序取前 3 逐 doc 计数 ≤3;score 递减
TC-PRIV-L1-25 FR-PRIV-004 ask_private 12k 截断 拼出的上下文字符数 > 12000 await ask_private 大 snippet 传给 complete_text 的 user 内容 ≤ 12000 字符;不抛异常 len(user)<=12000;可含截断标记
TC-PRIV-L1-26 FR-PRIV-004 ask_private LLM 40s 超时 monkeypatch complete_text 捕获 kwargs await ask_private question complete_text 被以 timeout=40 调用;其他参数(temperature/max_tokens/system 约束)符合任务书 调用参数断言:timeout==40;system 含「只能依据资料回答,不得编造」与「资料中未找到相关信息」
TC-PRIV-L1-27 FR-PRIV-004 ask_private 异常类型 三次 monkeypatch:search_chunks 抛 DocgenUnavailable;search_chunks 抛 Exception;complete_text 抛 LLMError await ask_private(pytest.raises) question DocgenUnavailable / Exception 向上抛(不吞);LLMError 向上抛 抛出的异常类型与注入一致(路由转 503/502 信封)
TC-PRIV-L1-28 FR-PRIV-005 schemas.py union 校验 TypeAdapter/MessageCreate 构造含 private_doc 块的消息 MessageCreate.model_validate / TypeAdapter(list[Block]) blocks=[{type:"private_doc", answer, items:[...]}] 校验通过;类型解析为 PrivateDocBlock;items 为 PrivateDocItem 列表 不抛 ValidationError;判别 type=="private_doc"
TC-PRIV-L1-29 FR-PRIV-005 schemas.py extra 未知字段 PrivateDocItem 带未知字段 model_validate items 含 {"x_unknown":1} 未知字段被忽略不报错;已知字段齐全 extra="allow" 行为;无异常
TC-PRIV-L1-30 FR-PRIV-004 services/docgen/llm.py timeout=None 兼容 monkeypatch _chat_once 捕获 timeout 参数 await complete_text(..., timeout=None) / timeout=40 两次调用 None → 走 env 现值(DOCGEN_LLM_TIMEOUT 或默认 300);40 → 透传 40 两次捕获 timeout 分别 ==env 现值 / 40;docgen 既有调用零影响
TC-PRIV-L1-31 FR-PRIV-004 services/docgen/client.py search_chunks MockTransport 注入 httpx client await search_chunks("q", top_k=8, min_score=0.30, timeout=12.0) q GET /api/vstore/search-chunks 参数带 q/top_k/min_score;返回 dict;不可达抛 DocgenUnavailable 请求路径与参数断言;异常类型断言

L1-front(chat store 竞态)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L1-32 FR-PRIV-003 chat.js _firePrivate 命中/未命中/异常三态静默 vi.mock api/private.js;pinia + 假会话(沿用 b2 chat.store.test.js 风格) 三次 send():askPrivate 分别返回命中/未命中/reject 同一提问 命中:aMsg 最终含 private_doc 块;未命中/reject:无块、无报错、QA 流程不受影响;三次均无 console.error 级用户可见错误 块存在性 + 无异常抛出 + lastResult 与既有 QA 断言一致
TC-PRIV-L1-33 FR-PRIV-005 chat.js 流未收口不丢块(pending) 命中响应在 QA complete 之前 resolve send()(_runStream 路径) 提问 响应先到 → 暂存 aMsg._privatePending;onComplete 整体替换 blocks 后 flush → 块仍在 终态 blocks 含 private_doc 块;顺序在 pipeline 之后
TC-PRIV-L1-34 FR-PRIV-005 chat.js onComplete 替换后仍出现 命中响应在 QA complete 之后 resolve send() 提问 替换发生后才 resolve:直接挂块 + 补 _persistFinal;终态含块 块存在且恰好 1 个(防重入)
TC-PRIV-L1-35 FR-PRIV-005 chat.js error 路径 flush QA 流 onError(如 UPSTREAM_DOWN) send() 提问 error 终态下私有命中块仍被挂上(flush 覆盖两条 error 路径) 终态 blocks 含 private_doc 块;消息 status=="error" 共存
TC-PRIV-L1-36 FR-PRIV-005 chat.js _appendPrivate 防重入 手动调用两次 _appendPrivate(aMsg, payload) 直接调 action payload blocks 中 private_doc 块仅 1 个 第二次调用为 no-op;len==1
TC-PRIV-L1-37 FR-PRIV-005 chat.js ask/search 两模式覆盖 分别走 _runStream 与 _runSearch send(text, {mode:"ask"}) / send(text, {mode:"search"}) 提问 两模式均触发 _firePrivate(askPrivate 被调 2 次);search 模式 blocks 赋值后含块 askPrivate 调用次数==2;终态块断言
TC-PRIV-L1-38 FR-PRIV-005 chat.js retry 覆盖 先 error 后 retry() retry() 提问 重试轮同样触发 _firePrivate;新 aMsg 不残留旧 pending askPrivate 再次被调;新消息 blocks 正确
TC-PRIV-L1-39 FR-PRIV-005 chat.js _flushPrivate 只清 pending 终态 flush 后再次 flush _flushPrivate(aMsg) 二次调用 无 块不被追加/清空 块仍 1 个;_privatePending 已清
TC-PRIV-L1-42 FR-PRIV-003 本轮上下文归属(TL 裁决行为级判据) 后消息的私有响应在前消息终态之后才 resolve(模拟跨轮迟到) 连续两次 send():第 1 轮 QA 完成且私有未命中终态、第 2 轮进行中,才 resolve 第 1 轮命中响应 同一提问 ×2 第 1 轮的迟到命中响应不得挂到第 2 轮消息上;第 2 轮终态与私有结果不受影响(串挂即 fail) 第 1 轮响应被丢弃或挂到第 1 轮消息;第 2 轮消息 blocks 不含来自第 1 轮的条目
TC-PRIV-L1-40 FR-PRIV-001 api/private.js askPrivate vi.mock http.js askPrivate({question, session_id}) 参数对象 POST /api/qa/private;body 含 top_k=8、min_score=0.30;timeout 60s 请求路径/body/opts 断言
TC-PRIV-L1-41 FR-PRIV-006 api/docgen.js removeLibrary 204 vi.mock http.js(del resolve 空 payload) removeLibrary(id) id 返回 {ok:true};不抛 JSON 解析错 204 空体被正确兜底

L2 接口测试(vstore 端点 / BFF 端点 / docgen 回归)

L2-vstore

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L2-01 FR-PRIV-002 GET /api/vstore/search-chunks 命中 已 ingest 含正文资料(chunk_count>0) curl GET /api/vstore/search-chunks?q=… q=正文关键词, top_k=5 HTTP 200;{ok:true, mode:"vector", query, count>0, items:[{chunk_id,doc_id,seq,snippet,score,doc_name,doc_category,doc_tags,as_template,created_at}]} count>0;item 字段全集存在;score ≥ min_score 默认 0.30
TC-PRIV-L2-02 FR-PRIV-002 search-chunks 未命中 库中无相关内容 q=无关词, min_score=0.30 q {ok:true, mode:"empty", count:0, items:[]} 无降级档(不返回向量库最新 N 条)
TC-PRIV-L2-03 FR-PRIV-002 search-chunks 空库 空 chunk 库 q=任意 q {ok:true, mode:"empty", items:[]} 不 500
TC-PRIV-L2-04 FR-PRIV-002 search-chunks top_k clamp 库中 ≥3 相关块 top_k=0 / top_k=99 q 0 → clamp 到 1(或 422);99 → 返回 ≤20 条 返回条数 ≤20;异常输入不 500
TC-PRIV-L2-05 FR-PRIV-002 search-chunks doc_ids 过滤 2 个 doc 各有命中 doc_ids=docA q 仅 docA 的 chunk 命中 所有 items.doc_id==docA
TC-PRIV-L2-06 FR-PRIV-002 search-chunks exclude_template 上传 1 模版(as_template=true)含关键词 exclude_template=true q=模版内容词 模版 chunk 不出现;普通资料不受影响 items 中无 as_template 行
TC-PRIV-L2-07 FR-PRIV-007 POST /api/vstore/rebuild-chunks 已有 chunk 行;删掉 chunks 目录后调用 POST rebuild-chunks 无 {ok:true, docs:N, chunks:M>0} chunks>0;N==vault 文档数
TC-PRIV-L2-08 FR-PRIV-007 GET /api/vstore/health chunk_count 已 ingest GET /health 无 {ok:true, count:…, backend:"local", chunk_count:…} chunk_count==ChunkIndex.count();与 ingest 返回一致
TC-PRIV-L2-09 FR-PRIV-007 重建期间可服务 重建 CLI 后台跑(由 TL 在 L2s 执行) 重建中并发 GET /health /search-chunks 无 两端点持续 200(锁内写文件不阻塞读) 无 5xx;健康返回正常

L2-BFF

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L2-10 FR-PRIV-004 POST /api/qa/private 命中 vstore chunk 库已就绪;DeepSeek key 有效 curl POST /api/qa/private {"question":"…与资料相关的问题","top_k":8,"min_score":0.30} HTTP 200;{ok:true,hit:true,mode:"vector",answer:非空 markdown,items:[{doc_id,name,seq,chunk_id,snippet,score,category,tags,created_at,ref}],elapsed_ms}(R2 起信封含 ok:true) answer 中每个 [资料n] 编号与 items[].ref 一一对应;answer ≤300 字;R3 起:LLM 收到用户问题与参考资料(user_message = 用户问题 + [资料n] 参考资料)
TC-PRIV-L2-11 FR-PRIV-004 /api/qa/private 未命中 库中无相关内容 curl POST {"question":"无关问题"} HTTP 200;{ok:true,hit:false,mode:"empty",answer:"",items:[]}(R2 起信封含 ok:true;自谦答语确定性改判 hit:false) 三态信封完整;无 error 字段
TC-PRIV-L2-12 FR-PRIV-004 /api/qa/private 参数校验 无 curl POST {"question":""} / 缺 question HTTP 422 {ok:false,error:{code:"VALIDATION"}}(R2 确认:空 question 与缺 question 均 422,question min_length=1) 统一 VALIDATION 信封(BFF 全局处理)
TC-PRIV-L2-13 FR-PRIV-004 /api/qa/private vstore 宕机 停 vstore(TL 配合) curl POST {"question":"…"} HTTP 503 {ok:false,error:{code:"UPSTREAM_DOWN", degraded:true}} 信封 code 与 degraded 断言
TC-PRIV-L2-14 FR-PRIV-004 /api/qa/private LLM 失败 monkeypatch/断 key 使 complete_text 抛(TL 配合) curl POST 命中但 LLM 失败 HTTP 502 {ok:false,error:{code:"UPSTREAM_ERROR"}} 信封 code 断言
TC-PRIV-L2-15 FR-PRIV-001 POST /api/docgen/library 上传(回归) vstore 正常 curl -F file=@资料.pdf -F name=… 小型 PDF HTTP 201;{ok:true, items:[{library_id,name,size,as_template}]} 上传成功返回条目;BFF doc_library 落行
TC-PRIV-L2-16 FR-PRIV-001 ingest 响应 chunk 字段(回归确认) 上传后取 ingest 返回/查 vstore 上传含正文 txt 文件 ingest item 含 chunk_count>0 且 chunk_ok==true;chunk_error=="" FR-PRIV-002 验收口径
TC-PRIV-L2-17 FR-PRIV-006 GET /api/docgen/library 列表 已上传 curl GET /api/docgen/library 无 {ok:true, items:[{id,name,category,tags,description,as_template,active,created_at}]} 列表含上传条目;字段全集存在
TC-PRIV-L2-18 FR-PRIV-006 DELETE /api/docgen/library/{id} 已上传 curl -X DELETE id HTTP 204(空体);doc_library 行删除;vstore 密文与 chunk 行删除 再 GET 单条 404;search-chunks 不含该 doc
TC-PRIV-L2-19 FR-PRIV-006 DELETE 不存在 id 无 curl -X DELETE 随机 id HTTP 404 NOT_FOUND 信封 不 500
TC-PRIV-L2-20 FR-PRIV-006 删除后不再被检索命中 删除后 GET search-chunks q=该资料内容 q items 不含该 doc_id FR-PRIV-006 验收口径

L2-docgen 回归

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L2-21 FR-PRIV-008 GET /api/vstore/search(doc 级)逐字段回归 改动前基线响应(编制时抓取/测试计划留存) curl GET /api/vstore/search?q=…&as_template=true q 响应字段与改动前逐字段一致(mode/items 键全集与顺序) FR-PRIV-002 验收口径;与基线 diff 空
TC-PRIV-L2-22 FR-PRIV-008 docgen 生成全链路(回归) 模版已上传 POST /api/docgen/generate {description, session_id, library_ids} {ok:true, artifact:{artifact_id,title,template_name,status:"done",…}} 生成成功;doc 级检索被使用(模版命中)
TC-PRIV-L2-23 FR-PRIV-008 docgen 导出(回归) artifact done POST /artifacts/{id}/export {format:"pdf"} format {ok:true, download_url};下载 200 导出闭环可用
TC-PRIV-L2-24 FR-PRIV-008 BFF /api/qa/search 行为零变化(回归) 改动前后 POST /api/qa/search {question} 响应契约与改动前一致(mode:"search", answer:"") 既有 b2/sdk test_qa 全绿

L2s 部署冒烟

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L2s-01 FR-PRIV-001 vstore 明文部署重启 TL 部署 重启 vstore → /health 无 启动无异常;health ok;含 chunk_count 无启动握手失败
TC-PRIV-L2s-02 FR-PRIV-007 CLI 存量重建 TL 执行 scripts/rebuild_chunks.py 后台运行 CLI(cwd=部署根) 存量 210 条 进度打印;完成汇总 {docs:210, chunks:M>0};期间 HTTP 可服务 chunks>0;无超时(不进 HTTP)
TC-PRIV-L2s-03 FR-PRIV-008 BFF 编译部署重启 TL 编译(白名单不变) 重启 BFF → /api/health 无 启动成功;private_docs 路由已挂载 health ok;POST /api/qa/private 返回三态之一
TC-PRIV-L2s-04 FR-PRIV-008 白名单未扩大 无 diff compile_sdk.sh 无 git diff 空 红线
TC-PRIV-L2s-05 FR-PRIV-005 前端构建 无 cd ui && npm run build 无 构建 0 error dist 可用

L3 前端组件

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L3-01 FR-PRIV-005 AssistantMessage private_doc 块渲染 mount AssistantMessage(b2 components.test.js 风格) message.blocks 含 private_doc 块 {type:"private_doc", answer:"…", items:[{ref:1,name,score,snippet}]} 卡体 .private-doc-block 存在;标题「📚 私有资料参考」;条目 [资料1]/名称/相关度/片段齐全 DOM 断言各项存在(R2 注:hit:false 改判后前端不产生 private_doc 块,本用例针对命中路径的块渲染)
TC-PRIV-L3-02 FR-PRIV-005 private_doc answer 空态 无 answer:"" + items 2 条 块 仅条目列表,无空白答区 答区元素不存在
TC-PRIV-L3-03 FR-PRIV-005 private_doc markdown 消毒 无 answer 含 <script>alert(1)</script> 与 加粗 块 script 被移除(DOMPurify);加粗渲染为 innerHTML 无 script;strong 存在
TC-PRIV-L3-04 FR-PRIV-006 PrivateLibraryPane 列表 vi.mock api/docgen.js(listLibrary 返回 2 条) mount PrivateLibraryPane 无 标题「📁 我的资料」+ 计数 2;两条 name/size/time 渲染 DOM 断言
TC-PRIV-L3-05 FR-PRIV-006 PrivateLibraryPane 空态 listLibrary 返回 items:[] mount 无 「尚无上传资料。在对话框拖拽 PDF / DOCX / TXT 上传。」 空态文案
TC-PRIV-L3-06 FR-PRIV-006 PrivateLibraryPane 删除+204 vi.mock(confirm 返回 true;removeLibrary 返回 {ok:true};listLibrary 先 2 后 1) mount → 点击删除 → confirm → 断言 无 调用 removeLibrary(id);列表刷新为 1 window.confirm 被调;刷新后 count==1
TC-PRIV-L3-07 FR-PRIV-006 PrivateLibraryPane 错误重试 listLibrary 首次 reject,二调成功 mount → 点重试 无 错误态显示重试按钮;点击后列表加载成功 重试按钮存在;重试后 items 渲染
TC-PRIV-L3-08 FR-PRIV-006 DocPane 空态挂载 mount DocPane(docgen.activeArtifact 为 null;mock PrivateLibraryPane 或全量挂载) mount 无 .doc-empty 存在;PrivateLibraryPane 挂载于其顶部(第一个子元素);有活动文档时隐藏 DOM 顺序断言;activeArtifact 非空时组件不渲染

L4 端到端(浏览器,TL 配合)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-L4-01 FR-PRIV-001/002 上传→检索闭环 浏览器打开生产 UI 拖拽上传 PDF → 查看「我的资料」→ 提问命中内容 PDF 上传成功条目出现;提问出「私有资料参考」块 全链闭环
TC-PRIV-L4-02 FR-PRIV-003/005 ask/search 两模式并行卡片 私有资料命中环境 分别 ask 与 search 模式提问 提问 两模式均出私有卡片,与 QA 答案并行展示不混排 卡片独立于 QA 答案
TC-PRIV-L4-03 FR-PRIV-003 未命中无感 私有资料命中环境 提问无关问题 无关问题 无任何私有块/提示;QA 答案正常 零痕迹(R2 起:无关查询 → hit:false,自谦答语改判)
TC-PRIV-L4-04 FR-PRIV-005 刷新恢复 出块后刷新页面 F5 → 重进会话 无 private_doc 块完整恢复(终态持久化) 块与 items 完整
TC-PRIV-L4-05 FR-PRIV-006 删除联动 我的资料列表 删除一条 → 再提问该资料内容 无 列表刷新;再提问不再命中该资料 删除后检索不到
TC-PRIV-L4-06 FR-PRIV-008 停 vstore 降级 停 vstore(TL 配合) 提问 QA + 观察 提问 QA 照常(doc 级不依赖 chunk);无私有块;无用户可见错误 FR-PRIV-008 验收口径
TC-PRIV-L4-07 FR-PRIV-001 上传无限制+进度条(回归) 浏览器 上传大文件 大文件 无大小拦截;进度条展示 FR-PRIV-001 验收口径

Lr 红线与回归(diff 检查)

编号 对应 FR 测试对象 前置条件 步骤 输入 期望输出 判据
TC-PRIV-Lr-01 FR-PRIV-008 QA 金标树 8091 零改动 无 git diff 基线..HEAD -- agent/qa 部署树 qa 无 diff 空 红线
TC-PRIV-Lr-02 FR-PRIV-008 前端红线零改动 无 git diff -- ui/src/stores/{modes,labels,bench}.js ui/src/api/qa.js ui/src/sse/parser.js ui/src/components/chat/PipelineCard.vue 无 diff 空 红线
TC-PRIV-Lr-03 FR-PRIV-008 vstore 红线零改动 无 git diff -- vstore/{retrieval,parsing,backend,crypto}.py 无 diff 空 红线
TC-PRIV-Lr-04 FR-PRIV-008 sdk 红线零改动 无 git diff -- sdk/app/routers/qa.py sdk/app/services/sse_relay.py sdk/tooling/compile/compile_sdk.sh 无 diff 空;白名单未扩大 红线
TC-PRIV-Lr-05 FR-PRIV-008 新模块无 yield/SSE 无 grep -n "yield" sdk/app/services/privateqa.py sdk/app/routers/private_docs.py 无 无 yield(断流崩溃根因回归) grep 空

2. 双向追溯矩阵

2.1 FR-PRIV → TC

FR 对应 TC
FR-PRIV-001 TC-PRIV-L1-40、TC-PRIV-L2-15、TC-PRIV-L2-16、TC-PRIV-L2s-01、TC-PRIV-L4-01、TC-PRIV-L4-07
FR-PRIV-002 TC-PRIV-L1-0107、TC-PRIV-L1-07b、TC-PRIV-L1-0813、TC-PRIV-L1-1417、TC-PRIV-L1-1819、TC-PRIV-L2-01~06、TC-PRIV-L2-16、TC-PRIV-L2-21、TC-PRIV-L4-01
FR-PRIV-003 TC-PRIV-L1-32、TC-PRIV-L1-37、TC-PRIV-L1-42、TC-PRIV-L4-02、TC-PRIV-L4-03
FR-PRIV-004 TC-PRIV-L1-2227、TC-PRIV-L1-30、TC-PRIV-L1-31、TC-PRIV-L2-1014
FR-PRIV-005 TC-PRIV-L1-28、TC-PRIV-L1-29、TC-PRIV-L1-3339、TC-PRIV-L2s-05、TC-PRIV-L3-0103、TC-PRIV-L4-02、TC-PRIV-L4-04
FR-PRIV-006 TC-PRIV-L1-20、TC-PRIV-L1-41、TC-PRIV-L2-1720、TC-PRIV-L3-0408、TC-PRIV-L4-05
FR-PRIV-007 TC-PRIV-L1-21、TC-PRIV-L2-07~09、TC-PRIV-L2s-02
FR-PRIV-008 TC-PRIV-L2-2124、TC-PRIV-L2s-03、TC-PRIV-L2s-04、TC-PRIV-L4-06、TC-PRIV-Lr-0105

2.2 调整对象 → TC(§1 清单每个文件/端点/函数)

文件/端点/函数 TC
vstore/chunks.py build_chunks TC-PRIV-L1-01~07、TC-PRIV-L1-07b
vstore/chunks.py ChunkIndex(upsert/remove/search/rebuild_all/count/原子写) TC-PRIV-L1-08~13、TC-PRIV-L2-07
vstore/embed.py embed_batch_chunked TC-PRIV-L1-14~17
vstore/vault.py add() chunk 钩子与返回字段 TC-PRIV-L1-18、TC-PRIV-L1-19、TC-PRIV-L2-16
vstore/vault.py delete() chunk 联动 TC-PRIV-L1-20、TC-PRIV-L2-18
vstore/vault.py rebuild_chunks() TC-PRIV-L1-21
vstore/api.py GET /api/vstore/search-chunks TC-PRIV-L2-01~06
vstore/api.py POST /api/vstore/rebuild-chunks TC-PRIV-L2-07、TC-PRIV-L2-09
vstore/api.py GET /api/vstore/health(chunk_count) TC-PRIV-L2-08
vstore/settings.py VSTORE_CHUNK_PATH 并入 TC-PRIV-L1-08(ChunkIndex 以 settings 路径构造,路径推导断言)
vstore/scripts/rebuild_chunks.py CLI TC-PRIV-L2s-02
存量 210 条重建 TC-PRIV-L2s-02、TC-PRIV-L2-07
sdk/app/services/privateqa.py ask_private TC-PRIV-L1-22~27
sdk/app/routers/private_docs.py POST /api/qa/private TC-PRIV-L2-10~14
sdk/app/schemas.py PrivateQARequest/PrivateDocItem/PrivateDocBlock/Block union TC-PRIV-L1-28、TC-PRIV-L1-29
sdk/app/main.py 路由注册 TC-PRIV-L2s-03
sdk/app/services/docgen/client.py search_chunks TC-PRIV-L1-31
sdk/app/services/docgen/llm.py complete_text timeout 参数 TC-PRIV-L1-30
ui/src/api/private.js askPrivate TC-PRIV-L1-40
ui/src/stores/chat.js _firePrivate TC-PRIV-L1-32、TC-PRIV-L1-42
ui/src/stores/chat.js pending/flush 竞态 TC-PRIV-L1-33、TC-PRIV-L1-34、TC-PRIV-L1-35、TC-PRIV-L1-39
ui/src/stores/chat.js _appendPrivate 防重入 TC-PRIV-L1-36
ui/src/stores/chat.js ask/search/retry 覆盖 TC-PRIV-L1-37、TC-PRIV-L1-38
ui/src/components/chat/AssistantMessage.vue private_doc 分支 TC-PRIV-L3-01~03
ui/src/i18n/private.js TC-PRIV-L3-01、TC-PRIV-L3-04(文案断言)
ui/src/components/bench/PrivateLibraryPane.vue TC-PRIV-L3-04~07
ui/src/components/bench/DocPane.vue 空态挂载 TC-PRIV-L3-08
ui/src/api/docgen.js removeLibrary(204) TC-PRIV-L1-41
docgen doc 级 /search 回归 TC-PRIV-L2-21、TC-PRIV-L2-22
上传/generate/导出回归 TC-PRIV-L2-15、TC-PRIV-L2-22、TC-PRIV-L2-23、TC-PRIV-L2-24
红线:QA 8091 TC-PRIV-Lr-01
红线:modes/labels/bench/api/qa/sse/parser/PipelineCard TC-PRIV-Lr-02
红线:vstore retrieval/parsing/backend/crypto TC-PRIV-Lr-03
红线:sdk qa.py/sse_relay.py/compile_sdk.sh TC-PRIV-Lr-04
断流崩溃(新模块无 yield) TC-PRIV-Lr-05
vstore/tests/test_chunks.py(测试文件本身) 本文 §3(pytest 全绿即覆盖)
ui/tests/private_docs/*(测试文件本身) 本文 §3(vitest 全绿即覆盖)

2.3 覆盖判据 §3 边界与异常逐项 → TC

判据项 TC
空库检索 TC-PRIV-L2-03
低分阈值 0.30 两侧 TC-PRIV-L1-12(两侧)、TC-PRIV-L1-13(低侧)、TC-PRIV-L2-02
超 top_k clamp TC-PRIV-L1-12、TC-PRIV-L2-04
每文档 200 块上限 TC-PRIV-L1-04
同 doc 去重每 doc≤3 TC-PRIV-L1-24
12k 上下文截断 TC-PRIV-L1-25
LLM 40s 超时 TC-PRIV-L1-26
vstore 宕机 TC-PRIV-L2-13、TC-PRIV-L4-06
LLM 宕机 TC-PRIV-L2-14
上传超长 PDF TC-PRIV-L1-04(超长切块)、TC-PRIV-L4-07(超长上传回归)
docgen 检索响应逐字段 TC-PRIV-L2-21
QA 8091 零改动 TC-PRIV-Lr-01
红线文件 diff 空 TC-PRIV-Lr-02~04

3. 测试文件骨架

(与本文档同批交付,本轮只写不跑。)

文件 覆盖 TC
vstore/tests/test_chunks.py TC-PRIV-L1-01~17、TC-PRIV-L1-07b(build_chunks/ChunkIndex/embed_batch_chunked;含 tmp_path 与假嵌入注入)
ui/tests/private_docs/chat.private.test.js TC-PRIV-L1-32~40、TC-PRIV-L1-42(chat store 竞态/防重入/两模式/retry/跨轮归属/askPrivate)
ui/tests/private_docs/components.private.test.js TC-PRIV-L3-01~03(AssistantMessage 块渲染)
ui/tests/private_docs/privateLibraryPane.test.js TC-PRIV-L3-04~07(面板)
ui/tests/private_docs/docPaneMount.test.js TC-PRIV-L3-08 + TC-PRIV-L1-41(空态挂载/removeLibrary 204)
ui/tests/private_docs/vitest.config.js 局部配置(b3 先例),依赖不改