conftest.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """pytest 全局装置。
  2. **必须在导入任何项目模块之前设置环境变量**:``config`` 在导入时就读
  3. ``.env`` 与环境变量并定稿,``db.db_conn`` 在导入时就建好连接适配器。
  4. 所以下面的 ``os.environ`` 赋值必须在 ``import config`` 之前发生——
  5. 这也是把它们放在文件最顶部、并设 ``DEEP_AGENT_SKIP_DOTENV`` 的原因:
  6. 不跳过 .env 的话,开发者本机的 .env 会覆盖这里的测试配置,
  7. 同一套测试在不同机器上跑出不同结果。
  8. """
  9. from __future__ import annotations
  10. import os
  11. import shutil
  12. import tempfile
  13. import uuid
  14. from pathlib import Path
  15. # ---------- 测试环境(必须在项目导入前设置)----------
  16. # 用 setdefault 而非直接赋值:外部已给出的 DB_DIALECT / DB_* 优先生效,
  17. # 这样同一套测试可以直接跑在 MySQL 上验证方言无关性——
  18. # DB_DIALECT=mysql DB_HOST=127.0.0.1 DB_PORT=3399 DB_USER=root DB_NAME=t python -m pytest
  19. _TMP_ROOT = Path(tempfile.mkdtemp(prefix="deep_agent_test_"))
  20. os.environ["DEEP_AGENT_ENV_PRIORITY"] = "none" # 完全忽略本机 .env
  21. os.environ.setdefault("DB_DIALECT", "sqlite")
  22. os.environ.setdefault("DB_SQLITE_PATH", str(_TMP_ROOT / "test.sqlite"))
  23. os.environ.setdefault("CHECKPOINTER", "memory") # 测试不需要落盘图状态
  24. os.environ["CTX_COMPACT_ENABLED"] = "0" # 不引入摘要中间件,测试更纯粹
  25. os.environ["CONTEXT_DEMO_ENABLED"] = "0" # 示例扩展由专门用例启用
  26. os.environ["OPENAI_API_KEY"] = "sk-test-key"
  27. os.environ["OPENAI_BASE_URL"] = "https://example.invalid"
  28. os.environ["TFAGENTS_DAILY_TOKEN_LIMIT"] = "0"
  29. os.environ["BILLING_ALLOW_UNPRICED"] = "0"
  30. os.environ["LOG_LEVEL"] = "WARNING"
  31. import pytest # noqa: E402
  32. import config # noqa: E402
  33. from db import db_conn # noqa: E402
  34. @pytest.fixture(scope="session", autouse=True)
  35. def _schema() -> None:
  36. """整个测试会话共用一份建好的表结构。"""
  37. db_conn.init_schema()
  38. @pytest.fixture(autouse=True)
  39. def _clean_tables():
  40. """每个用例前清空业务表,避免用例间互相污染。
  41. 删表数据而不是重建 schema:重建的代价在 sqlite 上也不小,而这里
  42. 只需要一个干净的状态。
  43. """
  44. for table in (
  45. "messages",
  46. "execution_runs",
  47. "request_keys",
  48. "billing_pending",
  49. "sessions",
  50. "session_context_usage",
  51. "context_archives",
  52. "billing_usage",
  53. "billing_pricing",
  54. "daily_quota",
  55. "user_preferences",
  56. "schedule_runs",
  57. "schedules",
  58. ):
  59. db_conn.execute(f"DELETE FROM {table}", ())
  60. # 工具注册表是模块级单例,清空后重新装载,避免用例顺序影响工具清单
  61. from tools import registry
  62. registry.reset_for_tests()
  63. registry.load_builtin_tools()
  64. yield
  65. @pytest.fixture(scope="session")
  66. def dialect() -> str:
  67. """当前测试跑的方言。用于需要区分方言的断言。"""
  68. return config.DB_DIALECT
  69. @pytest.fixture
  70. def pricing_ready():
  71. """确保单价表里有当前模型的条目,供计费相关用例使用。"""
  72. from billing import pricing
  73. pricing.seed_default_pricing()
  74. return pricing
  75. @pytest.fixture
  76. def session_id() -> str:
  77. """建一个测试会话,返回 session_id。"""
  78. from db import chat_store
  79. return chat_store.create_session("tester", "full", "测试会话")["id"]
  80. @pytest.fixture
  81. def unique_user() -> str:
  82. """每次调用返回一个新用户名,避免额度/流水跨用例串味。"""
  83. return f"u_{uuid.uuid4().hex[:8]}"
  84. def pytest_sessionfinish(session, exitstatus) -> None:
  85. """收尾清理临时目录。"""
  86. shutil.rmtree(_TMP_ROOT, ignore_errors=True)