mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
182 lines
8.0 KiB
Python
182 lines
8.0 KiB
Python
"""Shared test fixtures.
|
|
|
|
Isolate the persistent browser-skill store (and metrics) into throwaway temp
|
|
dirs for the whole test session, so tests never write skills/metrics into the
|
|
real ~/Library/Application Support/OpenSwarm/data tree (which would pollute the
|
|
dev machine and let a stale persisted skill leak across test runs).
|
|
|
|
The workflow fixtures below (isolated_workflows_data, reset_scheduler_state,
|
|
make_wf, fake_agent_manager) are deliberately NOT autouse: only the scheduled-
|
|
workflows suites opt in, so they never touch unrelated browser/service tests.
|
|
The two pre-existing workflow suites still carry their own local copies; new
|
|
suites lean on these shared ones instead of re-pasting the boilerplate.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import tempfile
|
|
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
# Tests mock claude_agent_sdk.query, not ClaudeSDKClient; the now-default-ON persistent client would route mocked turns onto a REAL CLI spawn and wedge the suite. Pin it OFF explicitly; the persistent path has its own tests (test_client_pool.py + live gates).
|
|
os.environ["OPENSWARM_PERSISTENT_CLIENT"] = "0"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_browser_state(monkeypatch):
|
|
skills_dir = tempfile.mkdtemp(prefix="os_skills_")
|
|
metrics_dir = tempfile.mkdtemp(prefix="os_metrics_")
|
|
playbook_dir = tempfile.mkdtemp(prefix="os_playbook_")
|
|
monkeypatch.setenv("OPENSWARM_BROWSER_SKILLS_DIR", skills_dir)
|
|
monkeypatch.setenv("OPENSWARM_BROWSER_METRICS_DIR", metrics_dir)
|
|
monkeypatch.setenv("OPENSWARM_BROWSER_PLAYBOOK_DIR", playbook_dir)
|
|
|
|
def _reset():
|
|
for mod in ("browser_skills", "browser_playbook"):
|
|
try:
|
|
m = __import__(f"backend.apps.agents.browser.{mod}", fromlist=[mod])
|
|
m.clear(wipe_disk=True)
|
|
except Exception:
|
|
pass
|
|
# metrics caches its dir at first use; drop it so each test writes where ITS env var points, not where the first test's pointed
|
|
try:
|
|
from backend.apps.agents.browser import browser_metrics as _bm
|
|
_bm.p_metrics_dir_cache = None
|
|
except Exception:
|
|
pass
|
|
_reset()
|
|
yield
|
|
_reset()
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_workflows_data(monkeypatch, tmp_path):
|
|
"""Point the workflow store at a throwaway tmpdir and start every test with
|
|
empty in-memory caches, so nothing leaks into a real install or across
|
|
tests. Mirrors the local fixtures in test_workflows_semantics.py and
|
|
test_schedule_e2e.py; opt in by requesting this fixture (or depend on it
|
|
from a file-local autouse)."""
|
|
from backend.apps.workflows import storage as p_storage
|
|
from backend.apps.workflows import audit as p_audit
|
|
from backend.apps.workflows import escalation as p_escalation
|
|
monkeypatch.setattr(p_storage, "DATA_DIR", str(tmp_path / "workflows"))
|
|
monkeypatch.setattr(p_storage, "RUNS_DIR", str(tmp_path / "workflows" / "runs"))
|
|
monkeypatch.setattr(p_storage, "PAUSED_FILE", str(tmp_path / "workflows" / "paused.json"))
|
|
monkeypatch.setattr(p_storage, "MISSED_FILE", str(tmp_path / "workflows" / "missed.json"))
|
|
monkeypatch.setattr(p_storage, "_workflow_cache", {})
|
|
monkeypatch.setattr(p_storage, "_runs_cache", {})
|
|
monkeypatch.setattr(p_storage, "_missed_cache", [])
|
|
monkeypatch.setattr(p_storage, "_cache_loaded", False)
|
|
monkeypatch.setattr(p_storage, "_paused", False)
|
|
monkeypatch.setattr(p_audit, "AUDIT_DIR", str(tmp_path / "workflows" / "audit"))
|
|
p_escalation._tasks.clear()
|
|
p_escalation._state.clear()
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def reset_scheduler_state(monkeypatch):
|
|
"""Reset the scheduler + executor module globals that otherwise survive
|
|
across tests and cause order-dependent flakes: a stale _wake Event bound to
|
|
a dead loop, a lingering _loop_task, a cached host tz, or a leftover entry
|
|
in the _running / control maps."""
|
|
from backend.apps.workflows import scheduler as p_scheduler
|
|
from backend.apps.workflows import executor as p_executor
|
|
p_scheduler._loop_task = None
|
|
p_scheduler._wake = asyncio.Event()
|
|
monkeypatch.setattr(p_scheduler, "_host_tz_cache", None)
|
|
p_executor._running.clear()
|
|
p_executor._run_control.clear()
|
|
p_executor._run_pause_override.clear()
|
|
yield
|
|
p_executor._running.clear()
|
|
p_executor._run_control.clear()
|
|
p_executor._run_pause_override.clear()
|
|
|
|
|
|
@pytest.fixture
|
|
def make_wf():
|
|
"""Factory for a Workflow with a sane default daily-9am-LA schedule;
|
|
override any field via kwargs."""
|
|
def p_build(**overrides):
|
|
from backend.apps.workflows.models import Workflow, ScheduleConfig, WorkflowStep
|
|
base = dict(
|
|
title="t",
|
|
steps=[WorkflowStep(text="hi")],
|
|
schedule=ScheduleConfig(
|
|
enabled=True, repeat_unit="day", repeat_every=1,
|
|
hour=9, minute=0, timezone="America/Los_Angeles",
|
|
),
|
|
)
|
|
base.update(overrides)
|
|
return Workflow(**base)
|
|
return p_build
|
|
|
|
|
|
class FakeAgentManager:
|
|
"""Stand-in for agent_manager so executor.execute() drives a full run
|
|
without a live LLM. Records launched configs + every step prompt sent, and
|
|
lets a test script the per-step terminal status (the value
|
|
_await_session_idle reads): default 'completed' (-> advance), or 'error' /
|
|
'stopped' to exercise the failure paths.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.sessions: dict[str, SimpleNamespace] = {}
|
|
self.tasks: dict[str, object] = {}
|
|
self.live_partial: dict[str, object] = {}
|
|
self.launched_configs: list[object] = []
|
|
self.sent_messages: list[str] = []
|
|
# statuses[i] is the session status after the i-th send_message; absent entries default to 'completed'. cost_usd lands on the run.
|
|
self.statuses: list[str] = []
|
|
self.cost_usd: float = 0.0
|
|
|
|
async def launch_agent(self, config):
|
|
self.launched_configs.append(config)
|
|
sid = f"sess-{len(self.sessions)}"
|
|
sess = SimpleNamespace(id=sid, status="completed", cost_usd=self.cost_usd, messages=[])
|
|
self.sessions[sid] = sess
|
|
return sess
|
|
|
|
async def send_message(self, session_id, text, hidden=False):
|
|
i = len(self.sent_messages)
|
|
self.sent_messages.append(text)
|
|
sess = self.sessions.get(session_id)
|
|
if sess is not None:
|
|
sess.status = self.statuses[i] if i < len(self.statuses) else "completed"
|
|
|
|
async def close_session(self, session_id):
|
|
self.sessions.pop(session_id, None)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_agent_manager(monkeypatch):
|
|
"""Patch the agent_manager seam (plus the ws + notifier side effects) that
|
|
executor.execute imports lazily, and hand back the FakeAgentManager so the
|
|
test can script statuses and assert on launched configs / sent steps."""
|
|
from backend.apps.agents import agent_manager as p_am
|
|
from backend.apps.agents.core.ws_manager import ws_manager as p_ws
|
|
from backend.apps.workflows import notifier as p_notifier
|
|
|
|
fake = FakeAgentManager()
|
|
monkeypatch.setattr(p_am.agent_manager, "sessions", fake.sessions)
|
|
monkeypatch.setattr(p_am.agent_manager, "tasks", fake.tasks)
|
|
monkeypatch.setattr(p_am.agent_manager, "live_partial", fake.live_partial)
|
|
monkeypatch.setattr(p_am.agent_manager, "launch_agent", fake.launch_agent)
|
|
monkeypatch.setattr(p_am.agent_manager, "send_message", fake.send_message)
|
|
monkeypatch.setattr(p_am.agent_manager, "close_session", fake.close_session)
|
|
|
|
async def p_noop_async(*args, **kwargs):
|
|
return None
|
|
|
|
from backend.apps.agents.manager.permissions import workflow_approval as p_wa
|
|
monkeypatch.setattr(p_wa, "set_workflow_approval_memory", lambda *a, **k: None)
|
|
monkeypatch.setattr(p_wa, "set_workflow_approval_step", lambda *a, **k: None)
|
|
monkeypatch.setattr(p_wa, "clear_workflow_approval_memory", lambda *a, **k: None)
|
|
monkeypatch.setattr(p_wa, "get_workflow_step_usage", lambda *a, **k: {})
|
|
monkeypatch.setattr(p_ws, "broadcast_global", p_noop_async)
|
|
monkeypatch.setattr(p_notifier, "notify_run_complete", p_noop_async)
|
|
return fake
|