conftest.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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["MAIN_FAN_ENABLED"] = "1"
  27. os.environ["SHANXI_GATEWAY_ENABLED"] = "0" # 测试禁止连接真实网关
  28. os.environ["RESULT_PLATFORM_ENABLED"] = "0" # 测试禁止连接真实结果平台
  29. os.environ["MAIN_FAN_AUTO_PUBLISH"] = "0"
  30. os.environ["OPENAI_API_KEY"] = "sk-test-key"
  31. os.environ["OPENAI_BASE_URL"] = "https://example.invalid"
  32. os.environ["TFAGENTS_DAILY_TOKEN_LIMIT"] = "0"
  33. os.environ["BILLING_ALLOW_UNPRICED"] = "0"
  34. os.environ["LOG_LEVEL"] = "WARNING"
  35. os.environ["ALGORITHM_JOB_DIR"] = str(_TMP_ROOT / "algorithm_jobs")
  36. import pytest # noqa: E402
  37. import config # noqa: E402
  38. from db import db_conn # noqa: E402
  39. @pytest.fixture(scope="session", autouse=True)
  40. def _schema() -> None:
  41. """整个测试会话共用一份建好的表结构。"""
  42. db_conn.init_schema()
  43. @pytest.fixture(autouse=True)
  44. def _clean_tables():
  45. """每个用例前清空业务表,避免用例间互相污染。
  46. 删表数据而不是重建 schema:重建的代价在 sqlite 上也不小,而这里
  47. 只需要一个干净的状态。
  48. """
  49. for table in (
  50. "test_lab_runs",
  51. "test_lab_assets",
  52. "main_fan_workflows",
  53. "main_fan_documents",
  54. "main_fan_analyses",
  55. "business_files",
  56. "business_push_targets",
  57. "business_jobs",
  58. "business_session_agents",
  59. "business_agent_state",
  60. "messages",
  61. "execution_runs",
  62. "request_keys",
  63. "billing_pending",
  64. "sessions",
  65. "session_context_usage",
  66. "context_archives",
  67. "billing_usage",
  68. "billing_pricing",
  69. "daily_quota",
  70. "user_preferences",
  71. "schedule_runs",
  72. "schedules",
  73. ):
  74. db_conn.execute(f"DELETE FROM {table}", ())
  75. # 工具注册表是模块级单例,清空后重新装载,避免用例顺序影响工具清单
  76. from tools import registry
  77. registry.reset_for_tests()
  78. registry.load_builtin_tools()
  79. yield
  80. @pytest.fixture(scope="session")
  81. def dialect() -> str:
  82. """当前测试跑的方言。用于需要区分方言的断言。"""
  83. return config.DB_DIALECT
  84. @pytest.fixture
  85. def pricing_ready():
  86. """确保单价表里有当前模型的条目,供计费相关用例使用。"""
  87. from billing import pricing
  88. pricing.seed_default_pricing()
  89. return pricing
  90. @pytest.fixture
  91. def session_id() -> str:
  92. """建一个测试会话,返回 session_id。"""
  93. from db import chat_store
  94. return chat_store.create_session("tester", "full", "测试会话")["id"]
  95. @pytest.fixture
  96. def unique_user() -> str:
  97. """每次调用返回一个新用户名,避免额度/流水跨用例串味。"""
  98. return f"u_{uuid.uuid4().hex[:8]}"
  99. def pytest_sessionfinish(session, exitstatus) -> None:
  100. """收尾清理临时目录。"""
  101. shutil.rmtree(_TMP_ROOT, ignore_errors=True)