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
318 lines
10 KiB
Python
318 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
from pathlib import Path
|
|
|
|
from typer.testing import CliRunner
|
|
|
|
from backend import cli
|
|
|
|
|
|
def test_live_runs_incremental_market_store_update_before_start(monkeypatch, tmp_path):
|
|
project_root = tmp_path
|
|
(project_root / ".env").write_text("FINNHUB_API_KEY=test\n", encoding="utf-8")
|
|
|
|
calls = []
|
|
|
|
monkeypatch.setattr(cli, "get_project_root", lambda: project_root)
|
|
monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None)
|
|
monkeypatch.setattr(cli, "run_data_updater", lambda project_root: calls.append(("run_data_updater", project_root)))
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_update_market_store",
|
|
lambda config_name, end_date=None: calls.append(("auto_update_market_store", config_name, end_date)),
|
|
)
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_enrich_market_store",
|
|
lambda config_name, end_date=None, lookback_days=120, force=False: calls.append(
|
|
("auto_enrich_market_store", config_name, end_date, lookback_days, force)
|
|
),
|
|
)
|
|
monkeypatch.setattr(cli.os, "chdir", lambda path: calls.append(("chdir", Path(path))))
|
|
|
|
def fake_run(cmd, check=True, **kwargs):
|
|
calls.append(("subprocess.run", cmd, check))
|
|
return 0
|
|
|
|
monkeypatch.setattr(cli.subprocess, "run", fake_run)
|
|
|
|
cli.live(
|
|
config_name="smoke_fullstack",
|
|
host="0.0.0.0",
|
|
port=8765,
|
|
trigger_time="now",
|
|
poll_interval=10,
|
|
clean=False,
|
|
enable_memory=False,
|
|
)
|
|
|
|
assert any(item[0] == "run_data_updater" for item in calls)
|
|
assert any(
|
|
item[0] == "auto_update_market_store" and item[1] == "smoke_fullstack"
|
|
for item in calls
|
|
)
|
|
assert any(
|
|
item[0] == "auto_enrich_market_store" and item[1] == "smoke_fullstack"
|
|
for item in calls
|
|
)
|
|
run_call = next(item for item in calls if item[0] == "subprocess.run")
|
|
assert run_call[1][:6] == [
|
|
cli.sys.executable,
|
|
"-u",
|
|
"-m",
|
|
"backend.main",
|
|
"--mode",
|
|
"live",
|
|
]
|
|
|
|
|
|
def test_backtest_runs_full_market_store_prepare_before_start(monkeypatch, tmp_path):
|
|
project_root = tmp_path
|
|
calls = []
|
|
|
|
monkeypatch.setattr(cli, "get_project_root", lambda: project_root)
|
|
monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None)
|
|
monkeypatch.setattr(cli, "run_data_updater", lambda project_root: calls.append(("run_data_updater", project_root)))
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_prepare_backtest_market_store",
|
|
lambda config_name, start_date, end_date: calls.append(
|
|
("auto_prepare_backtest_market_store", config_name, start_date, end_date)
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_enrich_market_store",
|
|
lambda config_name, end_date=None, lookback_days=120, force=False: calls.append(
|
|
("auto_enrich_market_store", config_name, end_date, lookback_days, force)
|
|
),
|
|
)
|
|
monkeypatch.setattr(cli.os, "chdir", lambda path: calls.append(("chdir", Path(path))))
|
|
|
|
def fake_run(cmd, check=True, **kwargs):
|
|
calls.append(("subprocess.run", cmd, check))
|
|
return 0
|
|
|
|
monkeypatch.setattr(cli.subprocess, "run", fake_run)
|
|
|
|
cli.backtest(
|
|
start="2026-03-01",
|
|
end="2026-03-10",
|
|
config_name="smoke_fullstack",
|
|
host="0.0.0.0",
|
|
port=8765,
|
|
poll_interval=10,
|
|
clean=False,
|
|
enable_memory=False,
|
|
)
|
|
|
|
assert any(item[0] == "run_data_updater" for item in calls)
|
|
assert any(
|
|
item[0] == "auto_prepare_backtest_market_store"
|
|
and item[1:] == ("smoke_fullstack", "2026-03-01", "2026-03-10")
|
|
for item in calls
|
|
)
|
|
assert any(
|
|
item[0] == "auto_enrich_market_store"
|
|
and item[1] == "smoke_fullstack"
|
|
and item[2] == "2026-03-10"
|
|
for item in calls
|
|
)
|
|
run_call = next(item for item in calls if item[0] == "subprocess.run")
|
|
assert run_call[1][:6] == [
|
|
cli.sys.executable,
|
|
"-u",
|
|
"-m",
|
|
"backend.main",
|
|
"--mode",
|
|
"backtest",
|
|
]
|
|
|
|
|
|
def test_live_cli_defaults_to_generic_run_label(monkeypatch, tmp_path):
|
|
project_root = tmp_path
|
|
(project_root / ".env").write_text("FINNHUB_API_KEY=test\n", encoding="utf-8")
|
|
|
|
calls = []
|
|
runner = CliRunner()
|
|
|
|
monkeypatch.setattr(cli, "get_project_root", lambda: project_root)
|
|
monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None)
|
|
monkeypatch.setattr(cli, "run_data_updater", lambda project_root: None)
|
|
monkeypatch.setattr(cli, "auto_update_market_store", lambda config_name, end_date=None: None)
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_enrich_market_store",
|
|
lambda config_name, end_date=None, lookback_days=120, force=False: None,
|
|
)
|
|
monkeypatch.setattr(cli.os, "chdir", lambda path: None)
|
|
|
|
def fake_run(cmd, check=True, **kwargs):
|
|
calls.append(cmd)
|
|
return 0
|
|
|
|
monkeypatch.setattr(cli.subprocess, "run", fake_run)
|
|
|
|
result = runner.invoke(cli.app, ["live", "--trigger-time", "now"])
|
|
|
|
assert result.exit_code == 0
|
|
assert calls
|
|
assert "--config-name" in calls[0]
|
|
config_index = calls[0].index("--config-name")
|
|
assert calls[0][config_index + 1] == "default_live_run"
|
|
|
|
|
|
def test_backtest_cli_defaults_to_generic_run_label(monkeypatch, tmp_path):
|
|
project_root = tmp_path
|
|
calls = []
|
|
runner = CliRunner()
|
|
|
|
monkeypatch.setattr(cli, "get_project_root", lambda: project_root)
|
|
monkeypatch.setattr(cli, "handle_history_cleanup", lambda config_name, auto_clean=False: None)
|
|
monkeypatch.setattr(cli, "run_data_updater", lambda project_root: None)
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_prepare_backtest_market_store",
|
|
lambda config_name, start_date, end_date: None,
|
|
)
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"auto_enrich_market_store",
|
|
lambda config_name, end_date=None, lookback_days=120, force=False: None,
|
|
)
|
|
monkeypatch.setattr(cli.os, "chdir", lambda path: None)
|
|
|
|
def fake_run(cmd, check=True, **kwargs):
|
|
calls.append(cmd)
|
|
return 0
|
|
|
|
monkeypatch.setattr(cli.subprocess, "run", fake_run)
|
|
|
|
result = runner.invoke(
|
|
cli.app,
|
|
["backtest", "--start", "2026-03-01", "--end", "2026-03-10"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert calls
|
|
assert "--config-name" in calls[0]
|
|
config_index = calls[0].index("--config-name")
|
|
assert calls[0][config_index + 1] == "default_backtest_run"
|
|
|
|
|
|
def test_main_parser_defaults_to_generic_run_label():
|
|
from backend.main import build_arg_parser
|
|
|
|
parser = build_arg_parser()
|
|
args = parser.parse_args([])
|
|
|
|
assert args.config_name == "default_run"
|
|
|
|
|
|
def test_ingest_enrich_runs_batch_enrichment(monkeypatch):
|
|
calls = []
|
|
|
|
monkeypatch.setattr(cli, "_resolve_symbols", lambda raw_tickers, config_name=None: ["AAPL", "MSFT"])
|
|
|
|
class DummyStore:
|
|
pass
|
|
|
|
monkeypatch.setattr(cli, "MarketStore", lambda: DummyStore())
|
|
monkeypatch.setattr(
|
|
cli,
|
|
"enrich_symbols",
|
|
lambda store, symbols, start_date=None, end_date=None, limit=200, analysis_source="local", skip_existing=True: calls.append(
|
|
("enrich_symbols", symbols, start_date, end_date, limit, analysis_source, skip_existing)
|
|
) or [
|
|
{
|
|
"symbol": symbol,
|
|
"news_count": 3,
|
|
"queued_count": 3,
|
|
"analyzed": 3,
|
|
"skipped_existing_count": 0,
|
|
"deduped_count": 0,
|
|
"llm_count": 0,
|
|
"local_count": 3,
|
|
}
|
|
for symbol in symbols
|
|
],
|
|
)
|
|
|
|
cli.ingest_enrich(
|
|
tickers=None,
|
|
start="2026-03-01",
|
|
end="2026-03-10",
|
|
limit=150,
|
|
force=False,
|
|
config_name="smoke_fullstack",
|
|
)
|
|
|
|
assert calls == [
|
|
("enrich_symbols", ["AAPL", "MSFT"], "2026-03-01", "2026-03-10", 150, "local", True)
|
|
]
|
|
|
|
|
|
def test_ingest_report_reads_market_store_report(monkeypatch):
|
|
calls = []
|
|
printed = []
|
|
|
|
monkeypatch.setattr(cli, "_resolve_symbols", lambda raw_tickers, config_name=None: ["AAPL"])
|
|
|
|
class DummyStore:
|
|
def get_enrich_report(self, symbols=None, start_date=None, end_date=None):
|
|
calls.append(("get_enrich_report", symbols, start_date, end_date))
|
|
return [
|
|
{
|
|
"symbol": "AAPL",
|
|
"raw_news_count": 10,
|
|
"analyzed_news_count": 8,
|
|
"coverage_pct": 80.0,
|
|
"llm_count": 5,
|
|
"local_count": 3,
|
|
"latest_trade_date": "2026-03-16",
|
|
"latest_analysis_at": "2026-03-16T09:00:00",
|
|
}
|
|
]
|
|
|
|
monkeypatch.setattr(cli, "MarketStore", lambda: DummyStore())
|
|
monkeypatch.setattr(cli, "get_explain_model_info", lambda: {"provider": "DASHSCOPE", "model_name": "qwen-max", "label": "DASHSCOPE:qwen-max"})
|
|
monkeypatch.setattr(cli, "llm_enrichment_enabled", lambda: True)
|
|
monkeypatch.setattr(cli.console, "print", lambda value: printed.append(value))
|
|
|
|
cli.ingest_report(
|
|
tickers=None,
|
|
start="2026-03-01",
|
|
end="2026-03-16",
|
|
config_name="smoke_fullstack",
|
|
only_problematic=False,
|
|
)
|
|
|
|
assert calls == [
|
|
("get_enrich_report", ["AAPL"], "2026-03-01", "2026-03-16")
|
|
]
|
|
assert printed
|
|
assert getattr(printed[0], "caption", "") == "Explain LLM: DASHSCOPE:qwen-max"
|
|
|
|
|
|
def test_filter_problematic_report_rows_keeps_low_coverage_and_no_llm():
|
|
rows = [
|
|
{
|
|
"symbol": "AAPL",
|
|
"coverage_pct": 100.0,
|
|
"llm_count": 2,
|
|
},
|
|
{
|
|
"symbol": "MSFT",
|
|
"coverage_pct": 80.0,
|
|
"llm_count": 1,
|
|
},
|
|
{
|
|
"symbol": "NVDA",
|
|
"coverage_pct": 100.0,
|
|
"llm_count": 0,
|
|
},
|
|
]
|
|
|
|
filtered = cli._filter_problematic_report_rows(rows)
|
|
|
|
assert [row["symbol"] for row in filtered] == ["MSFT", "NVDA"]
|