【Bug已解决】Stale `xfail` markers in `langchain-core` tests: `test_convert_to_openai_function_nested_v2…
发布时间:2026/8/19 9:30:18 作者:尧图编辑部 阅读量:1,286

【Bug已解决】Stalexfailmarkers inlangchain-coreteststest_convert_to_openai_function_nested_v2andtest_sync_in_sync_lambdaspass but are marked expected-to-fail 解决方案一、现象长什么样在langchain-core的测试套件里有两个测试用例被打了pytest.mark.xfail预期失败但实际上它们早就修好了、现在会通过test_convert_to_openai_function_nested_v2test_sync_in_sync_lambdas它们的表现如下在 CI 里跑pytest这两个用例不是绿色的 PASS也不是红色的 FAIL而是被标记为XPASSunexpectedly passing意外通过由于 pytest 默认把xfail_strictFalseXPASS 会被当成通过处理于是问题被悄悄掩盖——测试报告是全绿的没人发现这俩标记已经过时一旦哪天有人把xfail_strictTrue打开这是很多项目为了xfail 不能偷偷变绿而推荐的做法这两个用例会立刻变红CI 直接挂掉而且报错信息对排查者极不友好expected to fail but passed更糟的是因为标了xfail新来的贡献者会误以为这功能本来就是坏的从而绕开、甚至停止修复相关逻辑导致技术债被长期保留。一句话xfail标记标错了对象把已经通过的测试当成了已知失败形成僵尸标记。二、背景xfailexpected failure是 pytest 提供的一个机制用来标记我知道它会失败先别让它红。它在两种场景下是合理的上游依赖有 bug暂时绕不过先xfail等上游修一个尚不支持的特性先xfail锁住避免回归时无声无息。但xfail是一把双刃剑它本质上是在说这个测试现在不该绿。一旦底层代码被修复、功能被实现标记却没同步摘掉测试就会从 XFAIL符合预期变成 XPASS不符合预期。XPASS 在严格模式下直接失败在非严格模式下则只是静默通过——两种都违背了xfail的初衷。test_convert_to_openai_function_nested_v2是验证嵌套结构nested v2转 OpenAI function 的工具test_sync_in_sync_lambdas是验证在同步 lambda 里调用同步方法不会死锁/报错。这两个能力在langchain-core的某个版本之后就已经正常工作了但当时为了赶发布临时打的xfail没被清理于是长期残留。三、根因根因是测试维护纪律缺失而不是业务逻辑问题当初这两个测试因为某种临时原因可能是依赖版本、可能是实现未完成被标xfail后续 PR 修复了底层实现让测试能过了却只改了源码、没回头清理测试标记CI 的xfail_strict没打开XPASS 被当成 PASS 吞掉没有警报于是标记一直活着没有定期跑pytest --strict-markers或xfail_strictTrue的守护导致僵尸标记无法被发现。# 残留的错误标记示意 pytest.mark.xfail( reasonnested v2 conversion not supported yet, strictFalse, ) def test_convert_to_openai_function_nested_v2(): ...# 实际上功能早已支持标记应被删除 def test_convert_to_openai_function_nested_v2(): ...四、最小可运行复现下面用一个最小例子演示僵尸 xfail如何从静默 XPASS 变成严格模式下的红色失败import pytest def _add(a: int, b: int) - int: # 假设这个功能后来已经被正确实现 return a b pytest.mark.xfail(reasonadd 还没实现, strictFalse) def test_add_passes_but_marked_xfail(): # 功能其实已经好了但标记没摘 - XPASS被当 PASS 吞掉 assert _add(1, 2) 3 pytest.mark.xfail(reasonadd 还没实现, strictTrue) def test_add_strict_xfail(): # 打开 strict 后XPASS 直接变红 assert _add(1, 2) 3运行$ pytest -q # 默认 strictFalsetest_add_passes_but_marked_xfail 显示 XPASS整体 PASSED # 若 pytest.ini 设 xfail_stricttruetest_add_strict_xfail 直接 FAILED (XPASS)这正是langchain-core里那两个用例的真实处境平时静默 XPASS一旦严格化就爆红。五、解决方案第一层最小直接修复最小修复就是删掉过时的xfail标记让测试回归正常的 PASS/FAIL 语义# libs/core/tests/unit_tests/test_to_openai.py def test_convert_to_openai_function_nested_v2(): result _convert_to_openai_function_nested(sample_nested_v2) assert result[parameters][type] object assert nested in result[parameters][properties] # libs/core/tests/unit_tests/test_runnables.py def test_sync_in_sync_lambdas(): chain RunnableLambda(lambda x: x 1) assert chain.invoke(1) 2如果某个测试确实还想保留软失败的弹性比如依赖外部服务偶尔抖动应改用pytest.mark.flaky或显式的try/except pytest.skip而不是用xfail表达应该会过。六、解决方案第二层结构化改进为防止以后再出现僵尸xfail应当把标记清理纳入流程约束并提供一个可复用的审查工具。核心思路在 CI 里强制xfail_strictTrue并提供一个扫描脚本找出所有 XPASS 的用例清单。from dataclasses import dataclass from pathlib import Path import re from typing import List dataclass(frozenTrue) class LangChainXfailMarkerPolicy: xfail 标记审查策略扫描测试文件列出所有 xfail 装饰器。 配合 xfail_strictTrue 使用可让僵尸 xfail在 CI 直接变红 从而逼迫贡献者及时清理。 root: Path def list_xfail_markers(self) - List[str]: pattern re.compile(rpytest\.mark\.xfail) hits: List[str] [] for path in self.root.rglob(test_*.py): for lineno, line in enumerate(path.read_text().splitlines(), 1): if pattern.search(line): hits.append(f{path}:{lineno}: {line.strip()}) return hits def audit(self) - str: markers self.list_xfail_markers() if not markers: return OK: 未发现任何 xfail 标记。 return 需要人工复核以下 xfail 标记是否仍合理:\n \n.join(markers) def main() - None: policy LangChainXfailMarkerPolicy(rootPath(libs/core/tests)) print(policy.audit()) if __name__ __main__: main()同时在pyproject.toml/pytest.ini里加上[pytest] xfail_strict true这样任何意外通过的xfail都会立即让 CI 失败迫使开发者要么删除标记、要么真的让它失败杜绝静默残留。七、解决方案第三层断言 / CI 守护用一条 CI 规则把禁止僵尸 xfail锁死# .github/workflows/tests.yml (节选) - name: Run tests with strict xfail run: pytest -q --xfail-strict并提供回归测试验证清理后的用例能正常 PASSimport pytest from langchain_core.utils.function_calling import convert_to_openai_function def test_convert_to_openai_function_nested_v2(): # 确认嵌套 v2 结构能正确转换且不再被 xfail 屏蔽 spec { name: f, parameters: { type: object, properties: { outer: { type: object, properties: {inner: {type: string}}, } }, }, } converted convert_to_openai_function(spec) assert converted[parameters][properties][outer][properties][inner][type] string def test_sync_in_sync_lambdas(): from langchain_core.runnables import RunnableLambda chain RunnableLambda(lambda x: x * 2) assert chain.invoke(21) 42跑pytest --xfail-strict时这两个用例应当干净地显示PASSED不再有XPASS出现。八、排查清单在langchain-core测试目录里grep -rn xfail找出所有标记。跑pytest --xfail-strict看是否有用例报XPASSstrict 下为 FAILED。对每个 XPASS 用例判断功能是否已实现已实现则删标记未实现则保留并补全 reason。在pytest.ini里打开xfail_strict true让僵尸标记今后无法静默存活。用上面的LangChainXfailMarkerPolicy脚本定期扫描作为 PR 检查的一部分。新加xfail时必须在 reason 里写清为什么失败、预计何时摘掉。九、小结这两个xfail标记早已过时——底层功能修复后测试会意外通过XPASS却因为xfail_strictFalse被静默吞掉形成僵尸标记既掩盖了 CI 真实状态也让贡献者误判功能状态。最小修复是删掉过时标记更稳妥的做法是在pytest.ini打开xfail_strictTrue并配合LangChainXfailMarkerPolicy这样的扫描脚本把xfail 不能偷偷变绿写进 CI 守护从根本上杜绝同类问题复发。