Files
group_fqcd_jr/tests/conftest.py
T
qyqy 8cff6b35f9 fix(tests): tmp_path 落点改到仓库内,消除 33 个与缺陷无关的 setup 失败
问题:本机 `%TEMP%\pytest-of-Windows` 被权限更高的会话建过,当前用户无权写入,
于是所有用 `tmp_path` 的用例在 setup 阶段批量 ERROR(WinError 5)——实测 33 个,
散布在 offsite 附件预览、通知发送、promotion、worker 等处。这类噪声会让
"到底哪里坏了"完全看不出来(同事这批新用例首次把它暴露出来)。

修法:在 tests/conftest.py 覆盖 `tmp_path`,把根目录改到仓库内 `.workdir/pytest-tmp`
(已在 .gitignore)。语义不变——每个用例仍拿到一个**新建的空目录**(残留目录会污染断言)。
只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 私有构造,参数随版本变化
(实测直接实例化 `TempPathFactory(...)` 会 TypeError),而本仓库用例只用 `tmp_path`。

同时把 pyproject.toml 里那条 `basetemp = ...` 删掉:pytest **只在命令行认 basetemp**,
写在 ini 里会被静默忽略(实测无效),留着会让人误以为已配好。改为注释指向 conftest。

效果:不带任何参数 `pytest -q` 从「3 failed + 33 errors」变为「3 failed,0 error」。
2026-09-11 19:13:43 +08:00

106 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""测试共享夹具,以及测试期的数据库连接池隔离。
为什么在这里替换引擎(必须在导入任何 app 模块之前执行):
`app/infrastructure/db.py` 在导入时就建好了全局异步引擎,连接池里的 MySQL 连接绑定在
"建立它的那个事件循环"上。测试环境里同时存在多个循环——pytest-asyncio 每个用例一个、
每个 `TestClient` 一个 portal 线程循环、用例内 `asyncio.run(...)` 再建一个。连接被跨循环
复用时 SQLAlchemy/asyncmy 抛
`got Future <Future pending> attached to a different loop`,失败集合随执行顺序变化,
真实缺陷被噪声掩盖(同一用例全量跑失败、单条跑通过)。
NullPool 让每次取用都新建连接、归还即关闭,从根上消除跨循环复用。生产路径不受影响:
`app/infrastructure/db.py` 未改动,仍使用默认连接池。
"""
import asyncio
import sys
from typing import TYPE_CHECKING
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
from app.infrastructure import db as _db # noqa: E402
_db.engine = create_async_engine(
_db.engine.url, pool_pre_ping=True, poolclass=NullPool
)
_db.SessionFactory = async_sessionmaker( # type: ignore[assignment]
_db.engine, class_=AsyncSession, expire_on_commit=False
)
@pytest.fixture
def tmp_path() -> "Path":
"""把 `tmp_path` 的根目录改到**仓库内**(`.workdir/pytest-tmp`)。
为什么必须覆盖:pytest 默认在系统临时目录建 `%TEMP%\\pytest-of-<user>`。Windows 上
那个目录一旦被**权限更高的会话**(例如以管理员身份跑过一次测试)建过,当前用户就
无权再写入,于是**所有**用 `tmp_path` 的用例在 setup 阶段批量失败
(实测 33 个用例 ERROR、报 `PermissionError: [WinError 5]`),而它们与真实缺陷无关——
这种噪声会让"到底哪里坏了"完全看不出来。
覆盖后临时目录落在仓库内:不依赖系统临时目录权限,随 `.workdir/` 一起被 .gitignore
忽略,清理范围可见。语义不变——每个用例拿到一个**新建的空目录**(残留目录会让上一条
用例的产物污染断言)。
为什么只覆盖 `tmp_path` 而不动 `tmp_path_factory`:后者是 pytest 的私有构造
(`TempPathFactory.__init__` 的参数随版本变化,实测直接实例化会 `TypeError`),
而本仓库的用例只用 `tmp_path`。改动面越小,越不容易在下一次升级时炸。
"""
import shutil
from pathlib import Path
root = Path(".workdir") / "pytest-tmp"
root.mkdir(parents=True, exist_ok=True)
# 用目录数量推序号:不依赖 pytest 内部状态,重跑时自动接着编号。
index = len([item for item in root.iterdir() if item.is_dir()]) + 1
path = root / f"test{index}"
shutil.rmtree(path, ignore_errors=True)
path.mkdir(parents=True)
return path
from app.core.contracts import AgentDefinition, CoreResult, ResolvedAgentConfig # noqa: E402
from app.service.agent.base import BaseAgent # noqa: E402
from app.service.agent.factory import AgentFactory # noqa: E402
from app.service.agent.governance import review_output # noqa: E402
@pytest.fixture
def governance():
class TestGovernance:
async def resolve(self, definition, context):
return ResolvedAgentConfig(config_version="test", prompt_version="test",
model_endpoint="test")
async def recall(self, context):
return ()
async def review(self, result, context, config, memories, *, agent_type: str = ""):
# 透传 `agent_type`:门禁 F5(面向客户输出 100% 附固定话术)由它判定,
# 替身若不转发,`test_worker_runtime_mysql` 那条端到端断言会拿不到免责声明。
return review_output(result, context, config, memories, agent_type=agent_type)
return TestGovernance()
@pytest.fixture
def acceptance_registry(monkeypatch, governance):
"""Explicit test registration; production does not silently create business Agents."""
class TestAgent(BaseAgent):
async def handle(self, request, context):
return CoreResult(text="test result")
definition = AgentDefinition(agent_type="customer_service", version="test",
allowed_roles=("customer",), allowed_portals=("api",))
registry = AgentFactory(governance)
registry.register(definition, lambda _: TestAgent(definition))
monkeypatch.setattr("app.service.agent_run_application_service.get_agent_factory",
lambda: registry)
return registry