Files
roboco/tests/e2e_smoke/conftest.py
T

101 lines
4.0 KiB
Python
Raw Normal View History

"""e2e lifecycle smoke harness — collection gate + the stack fixture.
Scripted-agent smoke: an in-process RoboCo API (real routers, real
middleware, real gateway/choreographer/services) over the ephemeral test
Postgres, a local bare git origin standing in for GitHub, and a fake
GitHub REST layer whose merges are REAL git merges on that origin. A
deterministic driver calls the REAL MCP flow/do tool functions — no LLM
anywhere — so seam bugs (tool↔gate schema drift, squash merges, stale
refs, workspace routing) die here instead of in a live run.
Gating: excluded from the default suite (`make quality`); runs via
`make e2e-smoke` (sets ROBOCO_E2E_SMOKE=1). Needs the test Postgres
reachable and git on PATH, nothing else.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
import pytest
from tests.e2e_smoke.harness import build_e2e_stack
if TYPE_CHECKING:
from collections.abc import Iterator
from sqlalchemy.ext.asyncio import AsyncSession
from tests.e2e_smoke.harness import E2EStack
def pytest_collection_modifyitems(
config: pytest.Config, items: list[pytest.Item]
) -> None:
if os.environ.get("ROBOCO_E2E_SMOKE") == "1":
return
skip = pytest.mark.skip(reason="e2e smoke runs via `make e2e-smoke` only")
for item in items:
if "tests/e2e_smoke" in str(item.path):
item.add_marker(skip)
@pytest.fixture(autouse=True)
def _reset_lazy_db_holder() -> Iterator[None]:
"""Drop the app's lazy ``_DbHolder`` engine/factory between e2e tests.
Async smoke tests that drive production code using the global lazy engine
(``get_db_context`` / ``get_session_factory``) rebind ``_DbHolder`` to
their own function-scoped event loop. If a test leaves the holder bound to
its (now-dead) loop, the next consumer on a different loop — the uvicorn
server thread handling the next test's API call, or the next test itself —
reuses a factory bound to a dead loop and asyncpg raises
``RuntimeError: ... Future ... attached to a different loop``. Resetting
to ``None`` here makes every test start unbound so the first consumer
rebinds to its own loop. Disposal is intentionally not done here: an
engine bound to the uvicorn thread's loop cannot be awaited away from it.
"""
yield
from roboco.db import base as db_base
db_base._DbHolder.engine = None
db_base._DbHolder.session_factory = None
@pytest.fixture(autouse=True)
def _isolate_per_test_tables(e2e_stack: E2EStack) -> Iterator[None]:
"""Wipe the e2e DB between tests so per-test seeds never leak across tests.
``seed_company`` is session-cached and the canonical dev/qa/pm/... agent
rows are shared across every test by deterministic slug. A task any test
leaves ``pending`` assigned to that shared dev leaks into the session DB,
and the next test's ``give_me_work`` returns the leaked task instead of its
just-seeded one (the state-machine cascade: 8 tests got stale task "A"
from a prior test). The e2e app has no startup seed data, so the only
persisted state is what each test seeds. Truncating every table (agents
included — ``agents.current_task_id`` FK-references ``tasks``, so the
cycle means CASCADE on a partial set would wipe agents anyway) and
invalidating ``_COMPANY_CACHE`` gives complete per-test isolation: each
test re-seeds fresh canonical agents with new UUIDs against a clean DB.
"""
yield
from roboco.db.base import Base
from sqlalchemy import text
from tests.e2e_smoke.arcs import _COMPANY_CACHE
names = [t.name for t in Base.metadata.sorted_tables]
stmt = text(f"TRUNCATE TABLE {', '.join(names)} RESTART IDENTITY CASCADE")
async def _truncate(session: AsyncSession) -> None:
await session.execute(stmt)
e2e_stack.run_db(_truncate)
_COMPANY_CACHE.clear()
@pytest.fixture(scope="session")
def e2e_stack(
_test_database_url: str, tmp_path_factory: pytest.TempPathFactory
) -> Iterator[E2EStack]:
yield from build_e2e_stack(_test_database_url, tmp_path_factory)