mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
Auto-fixed 52 ruff F401 findings (unused imports) across 22 files
in backend/ and backend/tests/. Manually resolved 8 F841 unused
locals that ruff flagged as unsafe-fix:
- agent_manager.resume_session: drop dead hours_since_closed block.
- main.py mcp-meta + outputs-meta activate handlers: drop dead
reason = body.get("reason") binding (server ignores the field).
- dashboards.seed_demo, tools_lib.m365_device_login: keep _load(...)
call for its 404 side-effect, drop unused binding, add intent
comment.
- outputs.auto_run_output: keep `import anthropic` as availability
probe, mark with `# noqa: F401` and explanation.
- dead_code_scan._extract_ws_event_branches: drop vestigial
ws_handler_lines set (never written or read).
- test_browser_agent_unit.test_hash_tool_call_falls_back_to_repr:
drop the unused _Boom class+instance (the actual self-referential
bait is bad_input/bad_result; _Boom was never passed to the
function under test).
Result: 1184/1184 backend tests pass (1 deselected: pre-existing
sandbox-only git test). ruff --select F401,F811,F841 backend/ now
clean (was 60 findings).
Co-authored-by: Cursor <cursoragent@cursor.com>
249 lines
8.9 KiB
Python
249 lines
8.9 KiB
Python
"""Shared fixtures for the backend test suite.
|
|
|
|
The hard part of testing this app is environment setup — every sub-app
|
|
reads `OPENSWARM_DATA_DIR` (via `backend.config.paths`) at import time and
|
|
several lifespans kick off network/IO side effects (PostHog, 9Router,
|
|
MCP registry refresh). This conftest centralises that boilerplate so a
|
|
new endpoint test can be a one-liner:
|
|
|
|
def test_x(client):
|
|
r = client.get("/api/x/list")
|
|
assert r.status_code == 200
|
|
|
|
What's done here:
|
|
|
|
1. `OPENSWARM_DATA_DIR` is redirected to a fresh tmpdir BEFORE any
|
|
`backend.*` import. Same trick as the existing stress-test files.
|
|
2. PostHog is replaced with a Mock so `record(...)` never tries to
|
|
phone home.
|
|
3. `nine_router.ensure_running` is no-opped so the analytics lifespan
|
|
doesn't try to spawn a node subprocess.
|
|
4. Exposes a `client` fixture that builds a TestClient against the real
|
|
`backend.main:app`, drives the FastAPI lifespan (which seeds the
|
|
built-in modes / runs the dashboards migration), and pre-injects
|
|
the per-install bearer token.
|
|
5. Opt-in fixtures (`patched_skills_dir`, `stub_agent_loop`) for
|
|
individual test files that need extra isolation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Iterator
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pre-import bootstrap: redirect data dir BEFORE backend.* is imported.
|
|
# ---------------------------------------------------------------------------
|
|
# This MUST happen at module load time. backend/config/paths.py snapshots
|
|
# the env var into module-level constants (DATA_ROOT, SESSIONS_DIR, etc.),
|
|
# so any backend.* import after this line will pick up our tmp root.
|
|
|
|
_TMPROOT = tempfile.mkdtemp(prefix="openswarm-tests-")
|
|
os.environ["OPENSWARM_DATA_DIR"] = _TMPROOT
|
|
# Defensive: never let any test run hit the real OAuth helper service.
|
|
os.environ.setdefault("OPENSWARM_OAUTH_BASE_URL", "http://127.0.0.1:0")
|
|
|
|
# Re-home the user. Several agent paths fall back to `~/.openswarm/workspaces/...`
|
|
# when no default_folder/target_directory is configured (see
|
|
# `AgentManager.launch_agent`). On a CI runner that's likely fine, but on a
|
|
# developer machine it would write into the real home directory — and on
|
|
# sandboxed runs (e.g. agent tests under macOS sandbox) it fails outright with
|
|
# PermissionError. Pinning HOME inside the tmp root keeps the tests
|
|
# self-contained AND prevents accidental writes to the user's `~/.claude/skills`.
|
|
_FAKE_HOME = os.path.join(_TMPROOT, "home")
|
|
os.makedirs(_FAKE_HOME, exist_ok=True)
|
|
os.environ["HOME"] = _FAKE_HOME
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock external services BEFORE backend.main is imported.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _install_external_service_mocks() -> None:
|
|
"""Patch PostHog + 9Router so lifespans don't make outbound calls.
|
|
|
|
Called once at module load. We don't unwind these — they live for
|
|
the entire pytest session (matching the lifetime of the app fixture).
|
|
"""
|
|
# PostHog: replace the class so `Posthog(...)` returns a mock that
|
|
# silently absorbs `.capture(...)` / `.set(...)` / `.shutdown()`.
|
|
import posthog
|
|
_posthog_mock = MagicMock()
|
|
_posthog_mock.capture = MagicMock()
|
|
_posthog_mock.set = MagicMock()
|
|
_posthog_mock.shutdown = MagicMock()
|
|
posthog.Posthog = MagicMock(return_value=_posthog_mock)
|
|
|
|
|
|
_install_external_service_mocks()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Now safe to import backend.* — paths are pinned to _TMPROOT, posthog
|
|
# is a mock.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def app():
|
|
"""The real FastAPI app, lifespan NOT yet run.
|
|
|
|
Importing main is what registers all routers and middleware. We
|
|
additionally patch out the real 9Router and Skills sync triggers
|
|
that the analytics + settings lifespans spawn.
|
|
"""
|
|
# nine_router.ensure_running (called from analytics_lifespan) tries
|
|
# to pgrep / spawn a Node process. Replace with no-ops, matching
|
|
# each function's original sync/async shape (mismatch produces a
|
|
# "coroutine was never awaited" RuntimeWarning).
|
|
from backend.apps import nine_router as _nine
|
|
|
|
async def _async_noop(*args, **kwargs):
|
|
return None
|
|
|
|
def _sync_noop(*args, **kwargs):
|
|
return None
|
|
|
|
_nine.ensure_running = _async_noop # type: ignore[assignment]
|
|
_nine.stop = _sync_noop # type: ignore[assignment]
|
|
_nine.sync_gemini_api_key = _async_noop # type: ignore[assignment]
|
|
_nine.sync_openswarm_pro_as_claude = _async_noop # type: ignore[assignment]
|
|
|
|
from backend.main import app as _app
|
|
|
|
return _app
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def auth_token() -> str:
|
|
"""Initialise the per-install bearer token once for the session."""
|
|
from backend.auth import get_auth_token, init_auth_token
|
|
|
|
token = get_auth_token() or init_auth_token()
|
|
return token
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def auth_headers(auth_token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {auth_token}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_data_dirs(monkeypatch) -> Iterator[str]:
|
|
"""Wipe + recreate every per-feature data dir before each test.
|
|
|
|
Keeps tests order-independent. We don't move DATA_ROOT itself
|
|
(paths.py snapshotted that at import time); we just clear the
|
|
children so a previous test's mode/dashboard/tool/etc files don't
|
|
leak into the next.
|
|
"""
|
|
from backend.config import paths as _p
|
|
|
|
targets = [
|
|
_p.SESSIONS_DIR,
|
|
_p.MODES_DIR,
|
|
_p.DASHBOARDS_DIR,
|
|
_p.SETTINGS_DIR,
|
|
_p.TOOLS_DIR,
|
|
_p.OUTPUTS_DIR,
|
|
_p.OUTPUTS_WORKSPACE_DIR,
|
|
_p.SKILLS_WORKSPACE_DIR,
|
|
_p.DASHBOARD_LAYOUT_DIR,
|
|
]
|
|
for d in targets:
|
|
if os.path.exists(d):
|
|
shutil.rmtree(d, ignore_errors=True)
|
|
os.makedirs(d, exist_ok=True)
|
|
|
|
# Drop in-memory agent_manager state that survived a prior test.
|
|
try:
|
|
from backend.apps.agents.agent_manager import agent_manager as _am
|
|
|
|
_am.sessions.clear()
|
|
_am.tasks.clear()
|
|
except Exception:
|
|
pass
|
|
|
|
# Drop in-memory seq_log state too. Session ids are UUIDs so cross-test
|
|
# collisions are vanishingly rare, but a leaked ring buffer between
|
|
# tests can still produce confusing replay results in any test that
|
|
# inspects seq_log directly.
|
|
try:
|
|
from backend.apps.agents.seq_log import seq_log as _sl
|
|
|
|
_sl._per_session.clear()
|
|
except Exception:
|
|
pass
|
|
|
|
yield _p.DATA_ROOT
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app, auth_headers, tmp_data_dirs):
|
|
"""A pre-authenticated TestClient with FastAPI lifespan running.
|
|
|
|
The `with` block triggers startup events — this is what seeds the
|
|
built-in modes (modes_lifespan) and runs the dashboards migration
|
|
(dashboards_lifespan). Without it the modes endpoint would return
|
|
an empty list and the dashboards list would be empty too.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
|
|
# Per-test: seed the built-ins manually too. Lifespan only seeds
|
|
# them if the directory is empty, which `tmp_data_dirs` guarantees.
|
|
with TestClient(app) as tc:
|
|
tc.headers.update(auth_headers)
|
|
yield tc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Opt-in fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_skills_dir(monkeypatch, tmp_path) -> str:
|
|
"""Repoint backend.apps.skills at a tmp dir.
|
|
|
|
The real SKILLS_DIR lives at `~/.claude/skills` and is shared with
|
|
the user's actual Claude Code install. Tests must never touch it.
|
|
Hard-asserts that the patched path is inside the test tmpdir before
|
|
yielding.
|
|
"""
|
|
from backend.apps.skills import skills as skills_mod
|
|
|
|
skills_root = str(tmp_path / "skills")
|
|
os.makedirs(skills_root, exist_ok=True)
|
|
|
|
# Sanity gate — fail fast if pytest's tmp_path is somehow ~/.claude.
|
|
real_skills = os.path.expanduser("~/.claude/skills")
|
|
assert os.path.abspath(skills_root) != os.path.abspath(real_skills), (
|
|
"patched_skills_dir would clobber the real ~/.claude/skills"
|
|
)
|
|
|
|
monkeypatch.setattr(skills_mod, "SKILLS_DIR", skills_root)
|
|
monkeypatch.setattr(
|
|
skills_mod, "INDEX_PATH", os.path.join(skills_root, ".skills_index.json")
|
|
)
|
|
return skills_root
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_agent_loop(monkeypatch):
|
|
"""Replace AgentManager._run_agent_loop with a no-op coroutine.
|
|
|
|
Lets routes that internally `asyncio.create_task(self._run_agent_loop(...))`
|
|
succeed without spawning the Claude Code CLI / making real model calls.
|
|
"""
|
|
from backend.apps.agents.agent_manager import AgentManager
|
|
|
|
async def _noop(self, session_id, prompt, *args, **kwargs):
|
|
return None
|
|
|
|
monkeypatch.setattr(AgentManager, "_run_agent_loop", _noop)
|