Yuxi 测试审计与发布门禁 Oracle 收敛:从“检查源码“到“验证运行时事实“的测试治理实践
2026/9/17 21:37:23 网站建设 项目流程

Yuxi 测试审计与发布门禁 Oracle 收敛:从"检查源码"到"验证运行时事实"的测试治理实践

【免费下载链接】Yuxi可私有部署的多租户知识智能体平台:统一 RAG、知识图谱、多智能体、MCP/Skills、沙盒与权限管理。Self-hosted knowledge agent platform for RAG, knowledge graphs and multi-agent workflows.项目地址: https://gitcode.com/GitHub_Trending/yu/Yuxi

导读

本文基于 Yuxi 仓库中的架构决策记录 2026-09-07-test-suite-audit-follow-up.md,完整还原一次针对测试套件的"第二轮独立审计":如何识别出 Prompt 测试未验证真实组装结果、默认分块上限测试不足以排除错误默认值、benchmark reorder 测试依赖调度时序等证据边界缺陷,并逐一收敛为可独立证明运行时事实的 oracle。读完本文,你将掌握一套可复用的测试治理方法论——包括"负向断言必须作用在运行时组装结果上"、"用事件同步替代固定睡眠"、"用精确计数替代模糊上限"三条核心原则,以及 Backend 与 Web 两侧测试套件的取舍标准与验证基线,可直接用于你自己的测试套件审计。

背景:测试套件精简后的独立审查发现了什么

Yuxi 的测试套件此前已经过一次"精简",但在精简后的独立审查中,审计者仍发现若干证据边界不完整的问题,集中在四类场景:

问题具体表现后果
Prompt 测试不完整只检查源字符串,没有验证实际组装结果无法证明运行时 prompt 中不存在重复注入
默认分块上限测试不足只断言"硬上限",不校验默认值1.5 倍硬上限允许错误的默认分块值通过
benchmark reorder 测试依赖调度时序依赖固定延迟和调用计数事件循环调度变化会制造假失败或假绿
决策记录引用失效文件引用了已删除的测试文件文档与仓库状态脱节,误导后续读者

此外,审计还立下两条硬性纪律:不以测试名称相似或文件较长作为删除理由源码检查尚无等价行为覆盖时保留,并明确它只证明源码结构,不能冒充浏览器行为证据

该决策记录的类型被标记为simplification,Owner 指向backend/test/unit/agents/test_chatbot_prompt.py,说明这是一次"以简驭繁"的收敛——不是删减覆盖,而是把每条断言都钉在真正的观察边界上。

核心决策概览

针对上述问题,审计作出三项关键收敛决策:

  1. 在运行时组装后的 chatbot prompt 上保留html:preview不被重复注入的负向断言
  2. benchmark worker 用事件同步异常与后续调用,确保 reorder buffer 的测试不依赖固定睡眠
  3. 默认 512 token 配置使用精确 token 计数作为 oracle,并修正决策记录中已删除测试文件的合并路径。

每一条决策都对应一个被明确记录并否决的替代方案(详见下文各节),这正是 ADR 文档"决策可回溯、理由可审计"的体现。

决策一:Prompt 负向断言必须作用在运行时组装结果上

问题:只检查源字符串无法证明运行时不重复注入

旧的 Prompt 测试只检查prompt.py中定义的源字符串常量。审计判定该做法不可接受:源码中不存在html:preview与运行时组装后的 prompt 中不存在html:preview是两回事——组装逻辑(如拼接文件系统约束、日期、用户 system prompt)可能引入重复注入。

决策:对build_prompt_with_context的真实输出做负向断言

收敛后的测试位于 test_chatbot_prompt.py,其核心用例为:

from types import SimpleNamespace from yuxi.agents.buildin.chatbot.prompt import build_prompt_with_context def test_chatbot_prompt_declares_workspace_visibility_and_default_write_boundary(): prompt = build_prompt_with_context( SimpleNamespace( workdir_path="/home/gem/user-data/projects/11111111-1111-4111-8111-111111111111", system_prompt="", ) ) assert "可以读取其他 Project 目录作为参考" in prompt assert "未经用户明确要求,不得在当前 Project Workdir 之外" in prompt assert "/home/gem/user-data/agents/skills/" in prompt assert "html:preview" not in prompt

它调用真实的 build_prompt_with_context 函数,传入带workdir_pathsystem_prompt的上下文对象,对组装结果同时做正向断言(工作区可见性、默认写边界、个人 Skill 目录)和负向断言(html:preview不重复注入)。

从 prompt.py 的实现可以看到组装过程:build_prompt_with_context会拼接当前日期、基础 PROMPT、动态生成的<| 文件系统约束 |>段落以及用户的system_prompt,且当 context 缺少workdir_path时会抛出ValueError。这解释了为什么测试必须构造带workdir_path的上下文——它是该函数正常运行的前置条件。

替代方案:只检查源字符串(被拒绝)

文档明确记录:"只检查源字符串:拒绝,因为它不能证明实际运行时 prompt 没有重复注入。"这一原则可推广为一条通用的 oracle 选择准则:凡是断言"X 不发生",都应尽量放在最终产物上检查,而不是检查产生产物的中间常量。

决策二:benchmark worker 用事件同步替代固定睡眠

问题:reorder buffer 测试依赖调度时序

Yuxi 的 benchmark generation 采用多 worker 并发 + reorder buffer 按attempt_no连续 yield 的流式架构(见 benchmark_generation 相关实现)。旧的测试用固定延迟 + 调用计数来模拟"某 worker 先于另一 worker 产出"的时序,而事件循环的调度顺序一旦变化,就会出现假失败(该同步的没同步上)或假绿(该暴露的竞态没暴露)。

决策:用asyncio.Event显式同步

收敛后的测试位于 test_benchmark_generation.py,全部改用asyncio.Event做确定性同步。以异常收敛测试为例(第 403 行起):

@pytest.mark.asyncio async def test_iter_generated_benchmark_items_drains_reorder_buffer_on_exception(monkeypatch): """多 worker 下某 worker 抛异常时,reorder 缓冲中已产出但未按 attempt_no 连续 yield 的 item 经 drain 路径保全。""" monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: TrackingLlm()) monkeypatch.setattr(benchmark_generation, "kb_manager", NoQueryKnowledgeBase()) call_count = 0 first_call_started = asyncio.Event() release_first_call = asyncio.Event() async def fake_generate(**kwargs): nonlocal call_count call_count += 1 if call_count == 1: first_call_started.set() await release_first_call.wait() # 阻塞第一个调用,模拟延迟后抛异常 raise RuntimeError("worker error") ...

测试通过first_call_started.set()标记第一个 worker 已进入调用、release_first_call控制何时放行,从而精确构造"第一个 attempt 永不产出、其他 worker 的 item 卡在 reorder 缓冲"的时序,并断言异常 break 后 drain 路径把这些乱序 item 一并 yield:

assert items == [ {"query": "q2", "gold_answer": "a", "gold_chunk_ids": ["anchor_chunk"]}, {"query": "q3", "gold_answer": "a", "gold_chunk_ids": ["anchor_chunk"]}, {"query": "q4", "gold_answer": "a", "gold_chunk_ids": ["anchor_chunk"]}, ]

配套用例还验证了:

  • 真流式:第 2 次 LLM 调用未完成前第 1 条已产出(test_iter_generated_benchmark_items_yields_before_all_workers_finish),并配合asyncio.wait_for(collect_items(), timeout=5)防止死锁挂死;
  • 异常时取消其他 workertest_iter_generated_benchmark_items_cancels_other_workers_on_exception断言第二个 worker 收到CancelledErrorsecond_worker_cancelled.is_set())。

替代方案:保留固定延迟和调用计数(被拒绝)

文档明确记录:"保留固定延迟和调用计数:拒绝,因为事件循环调度变化会制造假失败或假绿。"事件同步的优点在于:无论调度器如何切换,测试只依赖"事件已设置"这一语义事实,而不是"过了多少毫秒"。

决策三:默认 512 token 上限用精确计数 oracle

问题:只断言硬上限会让错误默认值漏网

general parser 的分块流程存在两层上限:默认目标上限(512 token)与可选硬上限(hard_limit_token_num)。旧的测试只断言"任意 chunk 不超过硬上限",但若实现把默认值错误放宽(例如从 512 改成 1024),1.5 倍硬上限(768)依然成立,错误的默认值就会通过测试。

决策:锁定默认值的精确 token 计数

收敛后的 test_chunking_token_limit.py 新增了专门锁定默认值的用例:

def test_default_config_uses_512(self): doc = "测试\n" * 400 # 约 800 token;默认值错误放宽到 1024 时不会切分 chunks = general.chunk_markdown(doc) token_counts = [nlp.count_tokens(chunk) for chunk in chunks] assert token_counts == [514, 286]

该用例的巧妙之处在于:"测试\n" * 400约 800 token,若默认上限被错误放宽到 1024 则根本不会切分,测试即失败;同时精确断言切分结果是[514, 286]把默认值语义本身变成 oracle

该文件还通过_isolated_modulesfixture 用sys.modules占位隔离加载nlp.pyparsers/general.py,绕开 yuxi 包的重依赖链(langchain / pydantic / .env),并在运行后清理sys.modules避免污染其他测试——这同时印证了文档中"unit 测试仍可在不修改sys.path的情况下收集"的验证结论。同文件内的TestHardSplitByTokenLimit还覆盖了hard_limit_token_num=768时"短尾合并"的精确行为([512, 758])与max_chars=0时下限取 1 的边界(["a", "b", "c"])。

替代方案:只断言硬上限(被拒绝)

文档明确记录:"只断言硬上限:拒绝,因为 1.5 倍硬上限允许错误的默认分块值通过。"这条决策同时适用于语义分块(semantic parser):合并后的 test_semantic_chunking.py 中,test_empty_heading_preserves_current_title_context显式传入parser_config={"chunk_token_num": 512}验证空标题行不丢失标题上下文,test_truncated_heading_token_stream_is_ignored用 monkeypatch 制造截断的 heading token 流验证空结果。

决策四:Web 测试按实际观察边界取舍

Web 侧的审计遵循同一哲学——每条测试必须落在它真正能观察到的边界上

  • 保留:请求、状态、协议、解析和渲染结果的测试。例如"共享详情框架的运行时测试检查具名面板内容实际渲染、删除 Tab 后对应内容消失;在内存中移除动态内容插槽的负控会因面板内容缺失而失败",即通过运行时模板行为验证插槽、动态 Tab 与forceRender透传;HTML 预览尺寸仍在渲染输出上检查;工具参数解析保留实际输入结果测试。
  • 删除:仅冻结 CSS 数值、图标、旧文案和搬迁路径的检查——这些属于一次性迁移的产物,删除它们不代表视觉回归已有自动覆盖,文档对此有清醒声明。
  • 混合测试只删装饰性断言:保留可访问性、路由、数据隔离与迟到响应检查。
  • 源码检查策略:尚无等价行为覆盖时保留,但明确其边界——只证明源码结构,不能冒充浏览器行为证据,暂留的源码结构检查不得作为 UI 行为的证据。

文档还记录了 Web 守卫测试的收敛细节:守卫测试同时覆盖正常放行与拒绝;配置合并读取检查具体返回值和 Store 状态;模型优先级源码检查先确认表达式存在;轮询生命周期检查请求完成后再次调度及停止后的清理。且测试入口仍使用web/package.json的全目录 selector,未跳过或排除测试——避免用"配置文件排除"这种隐式手段掩盖覆盖缺口。

决策五:Backend 测试保留独立观察面与 QA parser 合并

保留原则:观察面不因文件名而删

Backend 侧确立的保留原则是:业务、权限、事务、队列、文件隔离、恢复和部署边界的独立观察面全部保留。文档特别强调:

  • live_api_cleanup(见 test_live_api_cleanup.py 与 test_live_api_cleanup_run_rows.py)、性能工具、Compose 契约(test_docker_compose_service_boundaries.py)和惰性导入检查均有明确 consumer
  • 不因文件名含 cleanup/tmp/source 或单文件用例少而删除——这是对"以名取人"式审计的明确否定。

合并:两个 QA parser 测试收敛为一个文件

两个 QA parser 测试合并为 test_qa_parser.py,共享一次隔离加载(_load_qa_parser通过importlib.util.spec_from_file_location按文件路径隔离加载ragflow_like/parsers/qa.py),并完整保留全部 oracle

能力面对应测试类/用例
结构化提取(Q:/A: 前缀、标题路径)TestSplitLongQaChunksTestAtxHeadingBoundary
围栏边界(~~~ / ``` 内不拆出虚构问答)TestPrefixFenceBoundary(含fence_with_info_string、不匹配围栏、未闭合围栏吸收等)
孤儿答案归属TestOrphanAnswer
超长切分限长TestChunkMarkdownLengthCap(走实现默认常量锁定"默认上限不超 embedding 承诺")
默认配置语义max_chars显式传参 + 默认路径双覆盖

其中_EMBEDDING_CHAR_LIMIT = 4000是独立于实现常量_QA_CHUNK_MAX_CHARS的保守字符兜底(对应 bge_m3 的 4096 token 上下文上限),避免自我引用 oracle——测试的期望值不应直接拷贝实现常量。TestSplitLongQaChunks还覆盖了含 tab 的问题保持完整、非英语答案标记不作分隔符、无答案标记的 tab 文本硬切等边界。

文档强调:没有合并观察边界不同的 unit、integration 和 E2E 测试——合并只发生在同一观察边界的文件之间。

第二轮收敛:死辅助对象清理与附件服务更名

决策记录还记录了第二轮收敛的三项动作:

  1. 删除 3 个同文件内无消费者的死辅助对象:静态引用检查确认FakeKnowledgeBase_ChildContext_make_thread_files在仓库内无消费者后才删除;
  2. 真实附件服务测试更名:从test_tmp_attachment_service.py更名为 test_attachment_service.py,消除"临时脚本"的错误信号,更名后仍收集相同的 16 个用例;
  3. 不删除仍被外部清理入口调用的兼容函数;队列策略正向测试改为断言规范化返回值(覆盖enqueue/reject/steer三个有效值的具体返回结果),而不是只验证不抛异常;两个 unit 测试移除无效的 cwdsys.path注入;示例问题测试的 3 个重复FakeKnowledgeBase定义收敛为 1 个按详情注入的 fake,异常和成功分支的返回断言保持不变。

此外,后端测试目录的静态审计确认6 个数据 fixture 均被解析或真实知识库路由使用,其中测试图片.png作为测试文档.docx的嵌入资源由转换结果间接校验(见 backend/test/data 目录),未删除仍被真实路由或解析测试使用的 fixture。

验证结果与发布门禁基线

收敛完成后,验证基线如下:

验证项结果
Prompt、分块和 benchmark generation 相关单元测试36 passed
ruff check与改动文件ruff format --check通过
已删除的test_semantic_chunking_empty_heading.py引用改为合并后的 test_semantic_chunking.py
Web 守卫测试同时覆盖正常放行与拒绝
配置合并读取检查具体返回值和 Store 状态
模型优先级源码检查先确认表达式存在
轮询生命周期请求完成后再次调度及停止后的清理
unit 测试收集无需修改sys.path即可收集

后果、旧能力禁令与重新引入条件

后果

  • 测试继续覆盖真实组装结果、worker 异常收敛和默认配置语义
  • 精确分块 oracle 依赖当前 tokenizer 的确定性结果,tokenizer 或分块策略变更时必须同步审阅语义 Owner 和决策记录
  • Web 的样式数值与一次性迁移不再由源码正则冻结,但这些删除不代表视觉回归已有自动覆盖
  • 队列、流式、API、文件隔离和设置交互测试继续独立维护;暂留的源码结构检查不能冒充浏览器行为证据。

旧能力禁令

文档用一句话立下红线:"测试不得退回只检查源常量、固定睡眠调度或模糊长度上限的证据。"这三个"退回"场景恰好对应本次收敛的三大决策,可作为任何测试评审的快速自检清单。

重新引入条件

文档规定,只有满足以下全部条件,才可替换当前 oracle:

  1. 新的测试仍能独立证明同一运行时事实
  2. 对调度有显式同步
  3. 区分配置回归

方法论提炼:可复用的测试审计清单

综合全文,可将本次收敛提炼为六条可迁移到其他项目的审计准则:

  1. 负向断言对准最终产物:断言"X 不发生"时,检查运行时组装/渲染后的结果,而非源常量;
  2. 用事件同步替代睡眠:所有并发时序测试使用asyncio.Event(或等价同步原语),并配合asyncio.wait_for超时防挂死;
  3. 默认值本身要成为 oracle:用精确计数断言默认配置的产物形态,防止错误默认值在硬上限掩护下漏网;
  4. 期望常量独立于实现常量:测试中的上限兜底值应来自独立推导(如模型上下文预算),避免自我引用;
  5. 按观察边界取舍测试:保留请求/状态/协议/渲染结果等真实观察边界,删除只冻结迁移产物的断言;源码结构检查可保留但必须声明其证据边界;
  6. 不以文件名或文件大小作删除依据:有明确 consumer 的测试(cleanup、性能、Compose 契约、惰性导入)必须保留;合并测试只发生在同一观察边界内,且不得改变 import 隔离与 oracle 语义。

【免费下载链接】Yuxi可私有部署的多租户知识智能体平台:统一 RAG、知识图谱、多智能体、MCP/Skills、沙盒与权限管理。Self-hosted knowledge agent platform for RAG, knowledge graphs and multi-agent workflows.项目地址: https://gitcode.com/GitHub_Trending/yu/Yuxi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询