Remove deprecated AnalystAgent, PMAgent, and RiskAgent classes. All agent creation now goes through UnifiedAgentFactory creating EvoAgent instances. - Delete backend/agents/analyst.py (169 lines) - Delete backend/agents/portfolio_manager.py (420 lines) - Delete backend/agents/risk_manager.py (139 lines) - Update all imports to use EvoAgent exclusively - Clean up unused imports across 25 files - Update tests to work with simplified agent structure Constraint: EvoAgent is now the single source of truth for all agent roles Constraint: UnifiedAgentFactory handles runtime agent creation Rejected: Keep legacy aliases | creates maintenance burden Confidence: high Scope-risk: moderate (affects agent instantiation paths) Directive: All new agent features must be added to EvoAgent, not legacy classes Not-tested: Kubernetes sandbox executor (marked with TODO)
285 lines
10 KiB
Python
285 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Integration tests for EvoAgent system.
|
|
|
|
These tests verify the integration between:
|
|
- UnifiedAgentFactory
|
|
- EvoAgent
|
|
- ToolGuardMixin
|
|
- Workspace-driven configuration
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
class TestUnifiedAgentFactoryIntegration:
|
|
"""Test UnifiedAgentFactory creates agents correctly."""
|
|
|
|
def test_factory_creates_analyst_with_workspace_config(self, tmp_path):
|
|
"""Test that factory creates EvoAgent with workspace config."""
|
|
from backend.agents.unified_factory import UnifiedAgentFactory
|
|
from backend.agents.base.evo_agent import EvoAgent
|
|
|
|
# Setup mock skills manager
|
|
class MockSkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def get_agent_active_root(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id / "skills" / "active"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def list_active_skill_metadata(self, config_name, agent_id):
|
|
return []
|
|
|
|
# Create workspace config
|
|
workspace_dir = tmp_path / "runs" / "test_config" / "agents" / "fundamentals_analyst"
|
|
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
(workspace_dir / "agent.yaml").write_text(
|
|
"prompt_files:\n - SOUL.md\n - CUSTOM.md\n",
|
|
encoding="utf-8",
|
|
)
|
|
(workspace_dir / "SOUL.md").write_text("System prompt content", encoding="utf-8")
|
|
(workspace_dir / "CUSTOM.md").write_text("Custom instructions", encoding="utf-8")
|
|
|
|
factory = UnifiedAgentFactory(
|
|
config_name="test_config",
|
|
skills_manager=MockSkillsManager(),
|
|
)
|
|
|
|
# Verify factory creates EvoAgent
|
|
agent = factory.create_analyst(
|
|
analyst_type="fundamentals_analyst",
|
|
model=MagicMock(),
|
|
formatter=MagicMock(),
|
|
)
|
|
|
|
assert isinstance(agent, EvoAgent)
|
|
assert agent.agent_id == "fundamentals_analyst"
|
|
assert agent.config_name == "test_config"
|
|
|
|
def test_factory_creates_risk_manager(self, tmp_path):
|
|
"""Test that factory creates risk manager EvoAgent."""
|
|
from backend.agents.unified_factory import UnifiedAgentFactory
|
|
from backend.agents.base.evo_agent import EvoAgent
|
|
|
|
class MockSkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def get_agent_active_root(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id / "skills" / "active"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def list_active_skill_metadata(self, config_name, agent_id):
|
|
return []
|
|
|
|
factory = UnifiedAgentFactory(
|
|
config_name="test_config",
|
|
skills_manager=MockSkillsManager(),
|
|
)
|
|
|
|
from unittest.mock import MagicMock
|
|
agent = factory.create_risk_manager(
|
|
model=MagicMock(),
|
|
formatter=MagicMock(),
|
|
)
|
|
|
|
assert isinstance(agent, EvoAgent)
|
|
assert agent.agent_id == "risk_manager"
|
|
|
|
def test_factory_creates_portfolio_manager(self, tmp_path):
|
|
"""Test that factory creates portfolio manager EvoAgent with financial params."""
|
|
from backend.agents.unified_factory import UnifiedAgentFactory
|
|
from backend.agents.base.evo_agent import EvoAgent
|
|
|
|
class MockSkillsManager:
|
|
def get_agent_asset_dir(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def get_agent_active_root(self, config_name, agent_id):
|
|
path = tmp_path / "runs" / config_name / "agents" / agent_id / "skills" / "active"
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
def list_active_skill_metadata(self, config_name, agent_id):
|
|
return []
|
|
|
|
factory = UnifiedAgentFactory(
|
|
config_name="test_config",
|
|
skills_manager=MockSkillsManager(),
|
|
)
|
|
|
|
from unittest.mock import MagicMock
|
|
agent = factory.create_portfolio_manager(
|
|
model=MagicMock(),
|
|
formatter=MagicMock(),
|
|
initial_cash=50000.0,
|
|
margin_requirement=0.3,
|
|
)
|
|
|
|
assert isinstance(agent, EvoAgent)
|
|
assert agent.agent_id == "portfolio_manager"
|
|
|
|
|
|
class TestToolGuardIntegration:
|
|
"""Test ToolGuardMixin integration with EvoAgent."""
|
|
|
|
def test_tool_guard_intercepts_guarded_tools(self):
|
|
"""Test that ToolGuard intercepts tools requiring approval."""
|
|
from backend.agents.base.tool_guard import ToolGuardMixin
|
|
|
|
class TestAgent(ToolGuardMixin):
|
|
def __init__(self):
|
|
self._init_tool_guard()
|
|
self.agent_id = "test_agent"
|
|
self.workspace_id = "test_workspace"
|
|
self.session_id = "test_session"
|
|
|
|
agent = TestAgent()
|
|
|
|
# Verify place_order is in guarded tools
|
|
assert agent._is_tool_guarded("place_order") is True
|
|
assert agent._is_tool_denied("execute_shell_command") is True
|
|
|
|
def test_tool_guard_approval_flow(self):
|
|
"""Test the full approval flow for a guarded tool."""
|
|
from backend.agents.base.tool_guard import (
|
|
ToolGuardStore,
|
|
ApprovalStatus,
|
|
)
|
|
|
|
store = ToolGuardStore()
|
|
|
|
# Create a pending approval record
|
|
record = store.create_pending(
|
|
tool_name="place_order",
|
|
tool_input={"ticker": "AAPL", "quantity": 100},
|
|
agent_id="test_agent",
|
|
workspace_id="test_workspace",
|
|
)
|
|
|
|
assert record.status == ApprovalStatus.PENDING
|
|
assert record.tool_name == "place_order"
|
|
|
|
# Approve the request with resolved_by
|
|
updated = store.set_status(record.approval_id, ApprovalStatus.APPROVED, resolved_by="test_user")
|
|
assert updated.status == ApprovalStatus.APPROVED
|
|
assert updated.resolved_by == "test_user"
|
|
|
|
def test_tool_guard_default_lists(self):
|
|
"""Test default guarded and denied tool lists."""
|
|
from backend.agents.base.tool_guard import (
|
|
DEFAULT_GUARDED_TOOLS,
|
|
DEFAULT_DENIED_TOOLS,
|
|
)
|
|
|
|
# Critical tools should be guarded
|
|
assert "place_order" in DEFAULT_GUARDED_TOOLS
|
|
assert "modify_position" in DEFAULT_GUARDED_TOOLS
|
|
assert "write_file" in DEFAULT_GUARDED_TOOLS
|
|
assert "edit_file" in DEFAULT_GUARDED_TOOLS
|
|
|
|
# Dangerous tools should be denied
|
|
assert "execute_shell_command" in DEFAULT_DENIED_TOOLS
|
|
|
|
|
|
class TestEvoAgentWorkspaceIntegration:
|
|
"""Test EvoAgent workspace-driven configuration."""
|
|
|
|
def test_evo_agent_loads_prompt_files_from_workspace(self, tmp_path, monkeypatch):
|
|
"""Test that EvoAgent loads prompt files from workspace directory."""
|
|
from backend.agents.base.evo_agent import EvoAgent
|
|
|
|
workspace_dir = tmp_path / "runs" / "demo" / "agents" / "test_analyst"
|
|
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create prompt files
|
|
(workspace_dir / "SOUL.md").write_text(
|
|
"You are a test analyst.", encoding="utf-8"
|
|
)
|
|
(workspace_dir / "INSTRUCTIONS.md").write_text(
|
|
"Additional instructions.", encoding="utf-8"
|
|
)
|
|
|
|
class MockToolkit:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
def register_agent_skill(self, path):
|
|
pass
|
|
|
|
monkeypatch.setattr(
|
|
"backend.agents.base.evo_agent.Toolkit",
|
|
MockToolkit,
|
|
)
|
|
|
|
class MockSkillsManager:
|
|
def get_agent_active_root(self, config_name, agent_id):
|
|
return workspace_dir / "skills" / "active"
|
|
|
|
def list_active_skill_metadata(self, config_name, agent_id):
|
|
return []
|
|
|
|
agent = EvoAgent(
|
|
agent_id="test_analyst",
|
|
config_name="demo",
|
|
workspace_dir=workspace_dir,
|
|
model=MagicMock(),
|
|
formatter=MagicMock(),
|
|
skills_manager=MockSkillsManager(),
|
|
prompt_files=["SOUL.md", "INSTRUCTIONS.md"],
|
|
)
|
|
|
|
# Verify prompts are loaded into system prompt
|
|
assert "You are a test analyst." in agent._sys_prompt
|
|
assert "Additional instructions." in agent._sys_prompt
|
|
|
|
|
|
class TestFactoryCaching:
|
|
"""Test UnifiedAgentFactory caching behavior."""
|
|
|
|
def test_factory_cache_per_config(self, monkeypatch):
|
|
"""Test that factory is cached per config name."""
|
|
from backend.agents.unified_factory import (
|
|
get_agent_factory,
|
|
clear_factory_cache,
|
|
)
|
|
|
|
# Clear any existing cache
|
|
clear_factory_cache()
|
|
|
|
mock_skills_manager = MagicMock()
|
|
|
|
factory1 = get_agent_factory("config_a", mock_skills_manager)
|
|
factory2 = get_agent_factory("config_a", mock_skills_manager)
|
|
factory3 = get_agent_factory("config_b", mock_skills_manager)
|
|
|
|
# Same config should return same instance
|
|
assert factory1 is factory2
|
|
# Different config should return different instance
|
|
assert factory1 is not factory3
|
|
|
|
def test_clear_factory_cache(self):
|
|
"""Test that clear_factory_cache removes all cached factories."""
|
|
from backend.agents.unified_factory import (
|
|
get_agent_factory,
|
|
clear_factory_cache,
|
|
)
|
|
|
|
mock_skills_manager = MagicMock()
|
|
|
|
factory1 = get_agent_factory("config_c", mock_skills_manager)
|
|
clear_factory_cache()
|
|
factory2 = get_agent_factory("config_c", mock_skills_manager)
|
|
|
|
# After clearing cache, should be new instance
|
|
assert factory1 is not factory2
|