Migrate all agent roles from Legacy to EvoAgent architecture: - fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst - risk_manager, portfolio_manager Key changes: - EvoAgent now supports Portfolio Manager compatibility methods (_make_decision, get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio) - Add UnifiedAgentFactory for centralized agent creation - ToolGuard with batch approval API and WebSocket broadcast - Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent) - Remove backend/agents/compat.py migration shim - Add run_id alongside workspace_id for semantic clarity - Complete integration test coverage (13 tests) - All smoke tests passing for 6 agent roles Constraint: Must maintain backward compatibility with existing run configs Constraint: Memory support must work with EvoAgent (no fallback to Legacy) Rejected: Separate PM implementation for EvoAgent | unified approach cleaner Confidence: high Scope-risk: broad Directive: EVO_AGENT_IDS env var still respected but defaults to all roles Not-tested: Kubernetes sandbox mode for skill execution
123 lines
4.7 KiB
Python
123 lines
4.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for the extracted agent service surface."""
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from backend.apps.agent_service import create_app
|
|
from backend.api import agents as agents_module
|
|
|
|
|
|
def test_agent_service_routes_include_control_plane_endpoints(tmp_path):
|
|
app = create_app(project_root=tmp_path)
|
|
|
|
paths = {route.path for route in app.routes}
|
|
|
|
assert "/health" in paths
|
|
assert "/api/status" in paths
|
|
assert "/api/workspaces" in paths
|
|
assert "/api/guard/pending" in paths
|
|
|
|
|
|
def test_agent_service_excludes_runtime_routes(tmp_path):
|
|
app = create_app(project_root=tmp_path)
|
|
paths = {route.path for route in app.routes}
|
|
|
|
assert "/api/runtime/start" not in paths
|
|
assert "/api/runtime/gateway/port" not in paths
|
|
|
|
|
|
def test_agent_service_status_includes_scope_metadata(tmp_path):
|
|
app = create_app(project_root=tmp_path)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/status")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["scope"]["design_time_registry"]["root"] == str(tmp_path / "workspaces")
|
|
assert payload["scope"]["runtime_assets"]["root"] == str(tmp_path / "runs")
|
|
assert "runs/<run_id>" in payload["scope"]["agent_route_note"]
|
|
|
|
|
|
def test_agent_service_read_routes(monkeypatch, tmp_path):
|
|
class _FakeSkillsManager:
|
|
project_root = tmp_path
|
|
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
return tmp_path / "runs" / config_name / "agents" / agent_id
|
|
|
|
def resolve_agent_skill_names(self, config_name, agent_id, default_skills=None):
|
|
return ["demo_skill"]
|
|
|
|
def list_agent_skill_catalog(self, config_name, agent_id):
|
|
return [
|
|
type(
|
|
"Skill",
|
|
(),
|
|
{
|
|
"skill_name": "demo_skill",
|
|
"name": "Demo Skill",
|
|
"description": "demo",
|
|
"version": "1.0.0",
|
|
"source": "builtin",
|
|
"tools": [],
|
|
},
|
|
)()
|
|
]
|
|
|
|
def load_agent_skill_document(self, config_name, agent_id, skill_name):
|
|
return {"skill_name": skill_name, "content": "# demo"}
|
|
|
|
class _FakeWorkspaceManager:
|
|
def load_agent_file(self, config_name, agent_id, filename):
|
|
return f"{config_name}:{agent_id}:{filename}"
|
|
|
|
monkeypatch.setattr(agents_module, "load_agent_profiles", lambda: {"portfolio_manager": {"skills": ["demo_skill"]}})
|
|
monkeypatch.setattr(agents_module, "get_agent_model_info", lambda agent_id: ("deepseek-v3.2", "DASHSCOPE"))
|
|
monkeypatch.setattr(
|
|
agents_module,
|
|
"load_agent_workspace_config",
|
|
lambda path: type(
|
|
"Cfg",
|
|
(),
|
|
{
|
|
"active_tool_groups": ["portfolio_ops"],
|
|
"disabled_tool_groups": [],
|
|
"enabled_skills": [],
|
|
"disabled_skills": [],
|
|
"prompt_files": ["SOUL.md", "MEMORY.md"],
|
|
},
|
|
)(),
|
|
)
|
|
monkeypatch.setattr(
|
|
agents_module,
|
|
"get_bootstrap_config_for_run",
|
|
lambda project_root, config_name: type("Bootstrap", (), {"agent_override": lambda self, agent_id: {}})(),
|
|
)
|
|
|
|
app = create_app(project_root=tmp_path)
|
|
app.dependency_overrides[agents_module.get_skills_manager] = lambda: _FakeSkillsManager()
|
|
app.dependency_overrides[agents_module.get_workspace_manager] = lambda: _FakeWorkspaceManager()
|
|
|
|
with TestClient(app) as client:
|
|
profile = client.get("/api/workspaces/demo/agents/portfolio_manager/profile")
|
|
skills = client.get("/api/workspaces/demo/agents/portfolio_manager/skills")
|
|
detail = client.get("/api/workspaces/demo/agents/portfolio_manager/skills/demo_skill")
|
|
workspace_file = client.get("/api/workspaces/demo/agents/portfolio_manager/files/MEMORY.md")
|
|
|
|
assert profile.status_code == 200
|
|
assert profile.json()["profile"]["model_name"] == "deepseek-v3.2"
|
|
assert profile.json()["scope_type"] == "runtime_run"
|
|
assert skills.status_code == 200
|
|
assert skills.json()["skills"][0]["skill_name"] == "demo_skill"
|
|
assert skills.json()["scope_type"] == "runtime_run"
|
|
assert detail.status_code == 200
|
|
assert detail.json()["skill"]["content"] == "# demo"
|
|
assert detail.json()["scope_type"] == "runtime_run"
|
|
assert workspace_file.status_code == 200
|
|
assert workspace_file.json()["content"] == "demo:portfolio_manager:MEMORY.md"
|
|
assert workspace_file.json()["scope_type"] == "runtime_run"
|
|
assert "runs/<run_id>" in workspace_file.json()["scope_note"]
|