#!/usr/bin/env python3 """Smoke-test the EvoAgent runtime rollout path. This script validates the current staged rollout shape: - start runtime via backend.api.runtime - confirm the gateway starts on an available port - confirm the gateway log shows the selected agent running as EvoAgent - confirm runtime_state.json is written - confirm guard approval API logic wakes a pending ToolApprovalRequest It intentionally avoids browser/front-end dependencies and does not require local HTTP callbacks. """ from __future__ import annotations import argparse import asyncio import json import os import sys import time from pathlib import Path import websocket PROJECT_ROOT = Path(__file__).resolve().parents[1] PYDEPS = PROJECT_ROOT / ".pydeps" _reordered_sys_path = [ str(PROJECT_ROOT), str(PYDEPS), ] for entry in list(sys.path): if entry in _reordered_sys_path: continue _reordered_sys_path.append(entry) sys.path[:] = _reordered_sys_path from fastapi import BackgroundTasks from backend.agents.base.tool_guard import TOOL_GUARD_STORE, ToolApprovalRequest from backend.api.guard import ApprovalRequest, approve_tool_call from backend.api.runtime import ( LaunchConfig, _is_gateway_running, get_runtime_state, start_runtime, stop_runtime, ) # All 6 agent roles supported by EvoAgent ALL_EVO_AGENT_ROLES = [ "fundamentals_analyst", "technical_analyst", "sentiment_analyst", "valuation_analyst", "risk_manager", "portfolio_manager", ] def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Smoke-test the staged EvoAgent runtime rollout.", ) parser.add_argument( "--agent-id", default="fundamentals_analyst", help="Agent id to enable via EVO_AGENT_IDS (use 'all' to test all 6 roles)", ) parser.add_argument( "--ticker", default="AAPL", help="Ticker to include in the smoke runtime bootstrap", ) parser.add_argument( "--max-wait-seconds", type=float, default=15.0, help="Maximum time to wait for gateway.log to appear", ) parser.add_argument( "--test-all-roles", action="store_true", help="Test all 6 EvoAgent roles sequentially", ) return parser.parse_args() def _wait_for_file(path: Path, timeout_seconds: float) -> None: deadline = time.time() + timeout_seconds while time.time() < deadline: if path.exists(): return time.sleep(0.2) raise TimeoutError(f"Timed out waiting for file: {path}") def _wait_for_initial_state(gateway_port: int, timeout_seconds: float) -> dict[str, object]: deadline = time.time() + timeout_seconds last_error: Exception | None = None while time.time() < deadline: try: ws = websocket.create_connection( f"ws://127.0.0.1:{gateway_port}", timeout=3, ) try: payload = json.loads(ws.recv()) return payload finally: ws.close() except Exception as exc: # pragma: no cover - best-effort smoke polling last_error = exc time.sleep(0.2) raise TimeoutError( f"Timed out waiting for gateway initial_state on port {gateway_port}: {last_error}" ) async def _run_smoke(agent_id: str, ticker: str, max_wait_seconds: float) -> dict[str, object]: previous_env = os.environ.get("EVO_AGENT_IDS") os.environ["EVO_AGENT_IDS"] = agent_id try: if _is_gateway_running(): await stop_runtime(force=True) response = await start_runtime( LaunchConfig( launch_mode="fresh", tickers=[ticker], schedule_mode="daily", interval_minutes=60, trigger_time="09:30", max_comm_cycles=1, initial_cash=100000.0, margin_requirement=0.0, enable_memory=False, mode="backtest", start_date="2025-11-01", end_date="2025-11-30", poll_interval=30, ), BackgroundTasks(), ) run_dir = Path(response.run_dir) log_path = run_dir / "logs" / "gateway.log" state_path = run_dir / "state" / "runtime_state.json" _wait_for_file(log_path, max_wait_seconds) _wait_for_file(state_path, max_wait_seconds) initial_state_payload = _wait_for_initial_state( response.gateway_port, max_wait_seconds, ) log_text = log_path.read_text(encoding="utf-8") state = json.loads(state_path.read_text(encoding="utf-8")) record = TOOL_GUARD_STORE.create_pending( tool_name="write_file", tool_input={"path": "smoke.txt"}, agent_id=agent_id, workspace_id=response.run_id, ) pending = ToolApprovalRequest( approval_id=record.approval_id, tool_name=record.tool_name, tool_input=record.tool_input, tool_call_id="smoke_call", ) record.pending_request = pending await approve_tool_call( ApprovalRequest( approval_id=record.approval_id, one_time=True, expires_in_minutes=30, ) ) result = { "run_id": response.run_id, "gateway_port": response.gateway_port, "gateway_running": _is_gateway_running(), "runtime_manager": get_runtime_state().runtime_manager is not None, "evo_log_present": f"EvoAgent initialized: {agent_id}" in log_text, "runtime_state_written": state_path.exists(), "registered_agents": [item["agent_id"] for item in state.get("agents", [])], "pending_request_approved": pending.approved is True, "ws_initial_type": initial_state_payload.get("type"), "ws_initial_tickers": ( (initial_state_payload.get("state") or {}).get("tickers") or [] ), } return result finally: if _is_gateway_running(): await stop_runtime(force=True) if previous_env is None: os.environ.pop("EVO_AGENT_IDS", None) else: os.environ["EVO_AGENT_IDS"] = previous_env def _verify_skills_loaded(log_text: str, agent_id: str) -> dict[str, bool]: """Verify that skills were loaded for the agent.""" return { "skills_loaded": f"Loading skills for {agent_id}" in log_text or "skills" in log_text.lower(), "tools_registered": "tool" in log_text.lower(), } async def _run_smoke_for_role(role: str, ticker: str, max_wait_seconds: float) -> dict[str, object]: """Run smoke test for a single agent role.""" print(f"\n>>> Testing EvoAgent role: {role}", file=sys.stderr) result = await _run_smoke( agent_id=role, ticker=ticker, max_wait_seconds=max_wait_seconds, ) result["agent_role"] = role result["success"] = ( result.get("evo_log_present", False) and result.get("runtime_state_written", False) and result.get("pending_request_approved", False) ) return result def main() -> int: args = _parse_args() if args.test_all_roles: # Test all 6 agent roles results = [] all_passed = True for role in ALL_EVO_AGENT_ROLES: try: result = asyncio.run( _run_smoke_for_role( role=role, ticker=args.ticker, max_wait_seconds=args.max_wait_seconds, ) ) results.append(result) if not result.get("success", False): all_passed = False print(f" FAILED: {role}", file=sys.stderr) else: print(f" PASSED: {role}", file=sys.stderr) except Exception as e: all_passed = False print(f" ERROR: {role} - {e}", file=sys.stderr) results.append({ "agent_role": role, "success": False, "error": str(e), }) summary = { "test_mode": "all_roles", "total_roles": len(ALL_EVO_AGENT_ROLES), "passed": sum(1 for r in results if r.get("success", False)), "failed": sum(1 for r in results if not r.get("success", False)), "all_passed": all_passed, "results": results, } print(json.dumps(summary, ensure_ascii=False, indent=2)) return 0 if all_passed else 1 else: # Test single agent result = asyncio.run( _run_smoke( agent_id=args.agent_id, ticker=args.ticker, max_wait_seconds=args.max_wait_seconds, ) ) print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())