Files
ZY-Agent/backend/tests/conftest.py
T
1445043649 361e653d37 feat(persistence): PR6 T6.1 — ThreadMetaRepository.create workspace_id sentinel
`create()` now accepts `workspace_id: str | None | _AutoSentinel = AUTO`
on both the SQL and in-memory implementations (and the abstract base).
AUTO resolves via `resolve_workspace_id()` from the workspace contextvar
that PR4 AuthMiddleware sets; explicit None bypasses for migration paths;
explicit str overrides the contextvar.

Test infrastructure:
- conftest gains an autouse `_auto_workspace_context` fixture mirroring
  the existing user fixture. Opt-out via `@pytest.mark.no_auto_workspace`.
- A SQLAlchemy `after_create` listener on `Base.metadata` seeds the
  matching `test-workspace-autouse` + `test-user-autouse` rows whenever
  `init_engine` runs `create_all()`, so the FK from threads_meta to
  workspaces resolves. Alembic migration tests bypass create_all and are
  unaffected, keeping real FK constraints under test.
- `test_thread_meta_workspace_filter.py` covers the three AUTO / explicit
  / None paths.

3 new tests pass; 115 existing thread_meta/run/feedback/run_event/owner
tests stay green.
2026-05-13 17:17:56 +08:00

232 lines
8.3 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.
"""
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"))
# Make 'fixtures.*' importable as plugin modules from this conftest.
sys.path.insert(0, str(Path(__file__).parent))
# Register fixture plugin modules so tests can request fixtures by name
# without ad-hoc imports. ``fixtures.postgres`` provides
# ``postgres_container`` (session-scoped) and ``postgres_url`` (per-test).
pytest_plugins = ["fixtures.postgres"]
# 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
# ---------------------------------------------------------------------------
# Auto-seed test workspace + user when Base.metadata.create_all() runs
# ---------------------------------------------------------------------------
#
# PR6 makes every business-row INSERT carry ``workspace_id`` (resolved
# from the autouse workspace contextvar = "test-workspace-autouse"). The
# Stage 0 schema has a NOT NULL FK from those rows to ``workspaces`` and
# from ``workspaces.owner_id`` to ``users``. Without the seed below,
# every legacy repo test would fail with a FOREIGN KEY error the moment
# it tries to insert a thread.
#
# We register an ``after_create`` hook on ``Base.metadata`` so that
# whenever ``init_engine`` finishes ``create_all()`` (the auto-create
# path used by tests and dev), the two anchor rows are present. Alembic
# migration tests don't trigger create_all so they are unaffected and
# keep exercising real FK constraints in isolation.
def _register_test_seed_listener() -> None:
"""Attach an after_create hook that seeds the autouse user + workspace."""
try:
from sqlalchemy import event
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from deerflow.persistence.base import Base
from deerflow.persistence.user.model import UserRow
from deerflow.persistence.workspace.model import WorkspaceRow
except ImportError:
return
from datetime import UTC, datetime
def _seed(_target, connection, **kw): # noqa: ARG001
tables = {t.name for t in kw.get("tables", []) or []}
if "users" not in tables or "workspaces" not in tables:
return
dialect = connection.dialect.name
now = datetime.now(UTC)
user_values = {
"id": "test-user-autouse",
"email": "test-user-autouse@local",
"password_hash": None,
"system_role": "user",
"created_at": now,
"oauth_provider": None,
"oauth_id": None,
"needs_setup": False,
"token_version": 0,
"default_workspace_id": None,
}
workspace_values = {
"id": "test-workspace-autouse",
"name": "Autouse Test Workspace",
"slug": "autouse-test",
"status": "active",
"owner_id": "test-user-autouse",
"created_at": now,
"updated_at": now,
}
if dialect == "sqlite":
user_stmt = sqlite_insert(UserRow.__table__).values(**user_values).on_conflict_do_nothing(index_elements=["id"])
ws_stmt = sqlite_insert(WorkspaceRow.__table__).values(**workspace_values).on_conflict_do_nothing(index_elements=["id"])
elif dialect == "postgresql":
user_stmt = pg_insert(UserRow.__table__).values(**user_values).on_conflict_do_nothing(index_elements=["id"])
ws_stmt = pg_insert(WorkspaceRow.__table__).values(**workspace_values).on_conflict_do_nothing(index_elements=["id"])
else:
return
connection.execute(user_stmt)
connection.execute(ws_stmt)
event.listen(Base.metadata, "after_create", _seed)
_register_test_seed_listener()
@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)
spec.loader.exec_module(module)
return 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 _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)
@pytest.fixture(autouse=True)
def _auto_workspace_context(request):
"""Inject a default ``test-workspace-autouse`` into the workspace contextvar.
Mirror of :func:`_auto_user_context`. PR6 adds ``workspace_id=AUTO``
sentinels to every repository method; without an autouse workspace
fixture every legacy persistence test would raise RuntimeError.
Opt-out via ``@pytest.mark.no_auto_workspace``.
"""
if request.node.get_closest_marker("no_auto_workspace"):
yield
return
try:
from deerflow.runtime.workspace_context import (
reset_current_workspace,
set_current_workspace,
)
except ImportError:
yield
return
workspace = SimpleNamespace(id="test-workspace-autouse", role="owner")
token = set_current_workspace(workspace)
try:
yield
finally:
reset_current_workspace(token)