deer-flow/backend/tests/conftest.py
rayhpeng fd74553f91 Merge branch 'main' into rayhpeng/hexagonal-feedback-slice
Rebase the feedback migration onto main's new chain tip again: main added
0009_webhook_dedupe (also chained after 0008_thread_operation_kind), so
0009_feedback_tags becomes 0010_feedback_tags with
down_revision=0009_webhook_dedupe, keeping the chain linear.

Conflicts resolved:
- Five head-pin tests take main's version with the pin bumped to
  0010_feedback_tags.
- test_thread_messages_page.py keeps this branch's feedback_service
  wiring over main's feedback_repo wiring, but stubs both service
  methods so the thread-grouped path main added coverage for stays
  stubbed (latest_per_run_in_thread alongside latest_for_runs).
- test_thread_messages_feedback.py keeps both sides' imports; each is
  used (Feedback for the fixture, EditReplayVisibility for the run
  manager stub).

Verified: 79 tests across the conflicted files plus the migration and
bootstrap suites, and 54 feedback tests, all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 07:13:25 +08:00

182 lines
6.4 KiB
Python

"""Test configuration for the backend test suite.
Sets up sys.path and pre-mocks modules that would cause circular import
issues when unit-testing lightweight config/registry code in isolation.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
# Make 'app' and 'deerflow' importable from any working directory
sys.path.insert(0, str(Path(__file__).parent.parent))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "scripts"))
# Shared test helpers (e.g. feedback_fakes) import as plain modules. pytest's
# default prepend import mode already puts this directory on the path, but not
# under --import-mode=importlib, so state the dependency explicitly.
sys.path.insert(0, str(Path(__file__).parent))
# Break the circular import chain that exists in production code:
# deerflow.subagents.__init__
# -> .executor (SubagentExecutor, SubagentResult)
# -> deerflow.agents.thread_state
# -> deerflow.agents.__init__
# -> lead_agent.agent
# -> subagent_limit_middleware
# -> deerflow.subagents.executor <-- circular!
#
# By injecting a mock for deerflow.subagents.executor *before* any test module
# triggers the import, __init__.py's "from .executor import ..." succeeds
# immediately without running the real executor module.
_executor_mock = MagicMock()
_executor_mock.SubagentExecutor = MagicMock
_executor_mock.SubagentResult = MagicMock
_executor_mock.SubagentStatus = MagicMock
_executor_mock.MAX_CONCURRENT_SUBAGENTS = 3
_executor_mock.get_background_task_result = MagicMock()
sys.modules["deerflow.subagents.executor"] = _executor_mock
@pytest.fixture()
def provisioner_module():
"""Load docker/provisioner/app.py as an importable test module.
Shared by test_provisioner_kubeconfig and test_provisioner_pvc_volumes so
that any change to the provisioner entry-point path or module name only
needs to be updated in one place.
"""
repo_root = Path(__file__).resolve().parents[2]
module_path = repo_root / "docker" / "provisioner" / "app.py"
spec = importlib.util.spec_from_file_location("provisioner_app_test", module_path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
previous_module = sys.modules.get(spec.name)
sys.modules[spec.name] = module
try:
spec.loader.exec_module(module)
yield module
finally:
if previous_module is None:
sys.modules.pop(spec.name, None)
else:
sys.modules[spec.name] = previous_module
# ---------------------------------------------------------------------------
# Auto-set user context for every test unless marked no_auto_user
# ---------------------------------------------------------------------------
#
# Repository methods read ``user_id`` from a contextvar by default
# (see ``deerflow.runtime.user_context``). Without this fixture, every
# pre-existing persistence test would raise RuntimeError because the
# contextvar is unset. The fixture sets a default test user on every
# test; tests that explicitly want to verify behaviour *without* a user
# context should mark themselves ``@pytest.mark.no_auto_user``.
@pytest.fixture(autouse=True)
def _reset_skill_storage_singleton():
"""Reset the SkillStorage singleton between tests to prevent cross-test contamination."""
try:
from deerflow.skills.storage import reset_skill_storage
except ImportError:
yield
return
reset_skill_storage()
try:
yield
finally:
reset_skill_storage()
@pytest.fixture(autouse=True)
def _reset_frozen_checkpoint_channel_mode(monkeypatch):
"""Reset the process-global frozen checkpoint channel mode between tests.
Production treats ``checkpoint_channel_mode`` as restart-required: the
first client/app freezes it for the process. The test suite builds many
clients and apps with different modes in one process, so the freeze must
not leak across tests. Mirrors the per-test ``monkeypatch.setattr``
resets already used in test_client.py / test_lead_agent_model_resolution.py.
"""
from deerflow.runtime import checkpoint_mode
monkeypatch.setattr(checkpoint_mode, "_frozen_checkpoint_channel_mode", None)
yield
@pytest.fixture(autouse=True)
def _reset_frozen_delta_snapshot_frequency(monkeypatch):
"""Reset the process-global frozen delta snapshot frequency between tests.
Same restart-required semantics as the checkpoint channel mode freeze
above: ``make_lead_agent`` freezes it from the app config, and the suite
builds many agents with different configs in one process.
"""
from deerflow.agents import thread_state
monkeypatch.setattr(thread_state, "_frozen_delta_snapshot_frequency", None)
yield
@pytest.fixture(autouse=True)
def _restore_title_config_singleton():
"""Reset ``_title_config`` to its pristine default after every test.
``AppConfig.from_file()`` writes the on-disk ``title`` block into the
module-level singleton (``config/app_config.py`` calls
``load_title_config_from_dict``). Any test that loads the real
``config.yaml`` therefore leaves the singleton in a state that
``test_title_middleware_core_logic.py`` does not expect; that suite
relies on the pristine ``TitleConfig()`` default (``enabled=True``).
We restore the default after every test so test files stay
independent regardless of order.
"""
try:
from deerflow.config.title_config import reset_title_config
except ImportError:
yield
return
try:
yield
finally:
reset_title_config()
@pytest.fixture(autouse=True)
def _auto_user_context(request):
"""Inject a default ``test-user-autouse`` into the contextvar.
Opt-out via ``@pytest.mark.no_auto_user``. Uses lazy import so that
tests which don't touch the persistence layer never pay the cost
of importing runtime.user_context.
"""
if request.node.get_closest_marker("no_auto_user"):
yield
return
try:
from deerflow.runtime.user_context import (
reset_current_user,
set_current_user,
)
except ImportError:
yield
return
user = SimpleNamespace(id="test-user-autouse", email="test@local")
token = set_current_user(user)
try:
yield
finally:
reset_current_user(token)