Add per-agent skill workspaces and TraderView management
This commit is contained in:
191
backend/tests/test_agent_workspace.py
Normal file
191
backend/tests/test_agent_workspace.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from backend.agents.prompt_factory import build_agent_system_prompt
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
from backend.agents.workspace_manager import WorkspaceManager
|
||||
|
||||
|
||||
class _DummyToolkit:
|
||||
def get_agent_skill_prompt(self):
|
||||
return ""
|
||||
|
||||
def get_activated_notes(self):
|
||||
return ""
|
||||
|
||||
|
||||
def test_workspace_manager_creates_extended_agent_files(tmp_path):
|
||||
manager = WorkspaceManager(project_root=tmp_path)
|
||||
|
||||
manager.initialize_default_assets(
|
||||
config_name="demo",
|
||||
agent_ids=["risk_manager"],
|
||||
analyst_personas={},
|
||||
)
|
||||
|
||||
asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
assert (asset_dir / "SOUL.md").exists()
|
||||
assert (asset_dir / "PROFILE.md").exists()
|
||||
assert (asset_dir / "AGENTS.md").exists()
|
||||
assert (asset_dir / "MEMORY.md").exists()
|
||||
assert (asset_dir / "HEARTBEAT.md").exists()
|
||||
assert (asset_dir / "agent.yaml").exists()
|
||||
assert (asset_dir / "skills" / "installed").is_dir()
|
||||
assert (asset_dir / "skills" / "active").is_dir()
|
||||
assert (asset_dir / "skills" / "disabled").is_dir()
|
||||
assert (asset_dir / "skills" / "local").is_dir()
|
||||
|
||||
|
||||
def test_agent_workspace_config_controls_prompt_files(tmp_path, monkeypatch):
|
||||
manager = WorkspaceManager(project_root=tmp_path)
|
||||
manager.initialize_default_assets(
|
||||
config_name="demo",
|
||||
agent_ids=["risk_manager"],
|
||||
analyst_personas={},
|
||||
)
|
||||
asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
(asset_dir / "SOUL.md").write_text("soul-line", encoding="utf-8")
|
||||
(asset_dir / "PROFILE.md").write_text("profile-line", encoding="utf-8")
|
||||
(asset_dir / "MEMORY.md").write_text("memory-line", encoding="utf-8")
|
||||
(asset_dir / "agent.yaml").write_text(
|
||||
"prompt_files:\n"
|
||||
" - SOUL.md\n"
|
||||
" - MEMORY.md\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from backend.agents import prompt_factory
|
||||
|
||||
monkeypatch.setattr(
|
||||
prompt_factory,
|
||||
"SkillsManager",
|
||||
lambda: SkillsManager(project_root=tmp_path),
|
||||
)
|
||||
|
||||
prompt = build_agent_system_prompt(
|
||||
agent_id="risk_manager",
|
||||
config_name="demo",
|
||||
toolkit=_DummyToolkit(),
|
||||
)
|
||||
|
||||
assert "soul-line" in prompt
|
||||
assert "memory-line" in prompt
|
||||
assert "profile-line" not in prompt
|
||||
|
||||
|
||||
def test_skills_manager_applies_agent_level_skill_toggles(tmp_path):
|
||||
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
||||
for skill_name in ("risk_review", "extra_guard"):
|
||||
skill_dir = builtin_root / skill_name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"# {skill_name}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
manager = WorkspaceManager(project_root=tmp_path)
|
||||
manager.initialize_default_assets(
|
||||
config_name="demo",
|
||||
agent_ids=["risk_manager"],
|
||||
analyst_personas={},
|
||||
)
|
||||
asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
(asset_dir / "agent.yaml").write_text(
|
||||
"enabled_skills:\n"
|
||||
" - extra_guard\n"
|
||||
"disabled_skills:\n"
|
||||
" - risk_review\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills_manager = SkillsManager(project_root=tmp_path)
|
||||
active_map = skills_manager.prepare_active_skills(
|
||||
config_name="demo",
|
||||
agent_defaults={"risk_manager": ["risk_review"]},
|
||||
)
|
||||
|
||||
active_dirs = active_map["risk_manager"]
|
||||
assert [path.name for path in active_dirs] == ["extra_guard"]
|
||||
assert (asset_dir / "skills" / "installed" / "extra_guard" / "SKILL.md").exists()
|
||||
assert (asset_dir / "skills" / "active" / "extra_guard" / "SKILL.md").exists()
|
||||
assert (asset_dir / "skills" / "disabled" / "risk_review" / "SKILL.md").exists()
|
||||
assert not (asset_dir / "skills" / "active" / "risk_review").exists()
|
||||
|
||||
|
||||
def test_agent_local_skill_is_activated_from_agent_workspace(tmp_path):
|
||||
manager = WorkspaceManager(project_root=tmp_path)
|
||||
manager.initialize_default_assets(
|
||||
config_name="demo",
|
||||
agent_ids=["risk_manager"],
|
||||
analyst_personas={},
|
||||
)
|
||||
asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
local_skill = asset_dir / "skills" / "local" / "local_guard"
|
||||
local_skill.mkdir(parents=True, exist_ok=True)
|
||||
(local_skill / "SKILL.md").write_text(
|
||||
"---\nname: 本地风控\ndescription: local skill\nversion: 1.0.0\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills_manager = SkillsManager(project_root=tmp_path)
|
||||
active_map = skills_manager.prepare_active_skills(
|
||||
config_name="demo",
|
||||
agent_defaults={"risk_manager": []},
|
||||
)
|
||||
|
||||
assert [path.name for path in active_map["risk_manager"]] == ["local_guard"]
|
||||
assert (asset_dir / "skills" / "active" / "local_guard" / "SKILL.md").exists()
|
||||
|
||||
|
||||
def test_prompt_includes_active_skill_metadata_summary(tmp_path, monkeypatch):
|
||||
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
||||
skill_dir = builtin_root / "extra_guard"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\n"
|
||||
"name: extra_guard\n"
|
||||
"description: This skill should be used when the user asks to \"run a risk check\".\n"
|
||||
"version: 1.0.0\n"
|
||||
"tools:\n"
|
||||
" - risk_ops\n"
|
||||
"---\n\n"
|
||||
"# Extra Guard\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
manager = WorkspaceManager(project_root=tmp_path)
|
||||
manager.initialize_default_assets(
|
||||
config_name="demo",
|
||||
agent_ids=["risk_manager"],
|
||||
analyst_personas={},
|
||||
)
|
||||
asset_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
(asset_dir / "agent.yaml").write_text(
|
||||
"enabled_skills:\n"
|
||||
" - extra_guard\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
skills_manager = SkillsManager(project_root=tmp_path)
|
||||
skills_manager.prepare_active_skills(
|
||||
config_name="demo",
|
||||
agent_defaults={"risk_manager": []},
|
||||
)
|
||||
|
||||
from backend.agents import prompt_factory
|
||||
|
||||
monkeypatch.setattr(
|
||||
prompt_factory,
|
||||
"SkillsManager",
|
||||
lambda: SkillsManager(project_root=tmp_path),
|
||||
)
|
||||
|
||||
prompt = build_agent_system_prompt(
|
||||
agent_id="risk_manager",
|
||||
config_name="demo",
|
||||
toolkit=_DummyToolkit(),
|
||||
)
|
||||
|
||||
assert "Active Skill Catalog" in prompt
|
||||
assert "This skill should be used when the user asks to \"run a risk check\"." in prompt
|
||||
assert "version: 1.0.0" in prompt
|
||||
assert "risk_ops" not in prompt
|
||||
@@ -382,3 +382,341 @@ async def test_refresh_market_store_for_watchlist_emits_system_messages(monkeypa
|
||||
assert gateway.state_sync.system_messages[0] == "正在同步自选股市场数据: AAPL, MSFT"
|
||||
assert "自选股市场数据已同步:" in gateway.state_sync.system_messages[1]
|
||||
assert "AAPL prices=3 news=4" in gateway.state_sync.system_messages[1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_get_agent_skills_returns_statuses(tmp_path):
|
||||
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
||||
for name in ("risk_review", "extra_guard"):
|
||||
skill_dir = builtin_root / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"---\nname: {name}\ndescription: {name} desc\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
agent_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
(agent_dir / "agent.yaml").write_text(
|
||||
"enabled_skills:\n"
|
||||
" - extra_guard\n"
|
||||
"disabled_skills:\n"
|
||||
" - risk_review\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
await gateway._handle_get_agent_skills(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager"},
|
||||
)
|
||||
|
||||
assert websocket.messages[-1]["type"] == "agent_skills_loaded"
|
||||
statuses = {
|
||||
row["skill_name"]: row["status"]
|
||||
for row in websocket.messages[-1]["skills"]
|
||||
}
|
||||
assert statuses["extra_guard"] == "enabled"
|
||||
assert statuses["risk_review"] == "disabled"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_get_agent_profile_returns_model_and_tool_groups(monkeypatch, tmp_path):
|
||||
agent_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager"
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
(agent_dir / "agent.yaml").write_text(
|
||||
"prompt_files:\n"
|
||||
" - SOUL.md\n"
|
||||
" - MEMORY.md\n"
|
||||
"active_tool_groups:\n"
|
||||
" - risk_ops\n"
|
||||
"disabled_tool_groups:\n"
|
||||
" - legacy_group\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
monkeypatch.setattr(
|
||||
gateway_module,
|
||||
"load_agent_profiles",
|
||||
lambda: {"risk_manager": {"skills": ["risk_review"], "active_tool_groups": ["risk_ops", "legacy_group"]}},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway_module,
|
||||
"get_agent_model_info",
|
||||
lambda agent_id: ("gpt-4o-mini", "OPENAI"),
|
||||
)
|
||||
|
||||
class _Bootstrap:
|
||||
@staticmethod
|
||||
def agent_override(_agent_id):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
gateway_module,
|
||||
"get_bootstrap_config_for_run",
|
||||
lambda project_root, config_name: _Bootstrap(),
|
||||
)
|
||||
|
||||
await gateway._handle_get_agent_profile(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager"},
|
||||
)
|
||||
|
||||
assert websocket.messages[-1]["type"] == "agent_profile_loaded"
|
||||
profile = websocket.messages[-1]["profile"]
|
||||
assert profile["model_name"] == "gpt-4o-mini"
|
||||
assert profile["model_provider"] == "OPENAI"
|
||||
assert profile["prompt_files"] == ["SOUL.md", "MEMORY.md"]
|
||||
assert profile["active_tool_groups"] == ["risk_ops"]
|
||||
assert profile["disabled_tool_groups"] == ["legacy_group"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_get_skill_detail_returns_markdown_body(tmp_path):
|
||||
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "risk_review"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: 风险审查\ndescription: 说明\nversion: 1.0.0\n---\n# 风险审查\n\n完整正文\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
await gateway._handle_get_skill_detail(
|
||||
websocket,
|
||||
{"skill_name": "risk_review"},
|
||||
)
|
||||
|
||||
assert websocket.messages[-1]["type"] == "skill_detail_loaded"
|
||||
assert websocket.messages[-1]["skill"]["name"] == "风险审查"
|
||||
assert websocket.messages[-1]["skill"]["version"] == "1.0.0"
|
||||
assert websocket.messages[-1]["skill"]["content"] == "# 风险审查\n\n完整正文"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_get_skill_detail_prefers_agent_local_skill(tmp_path):
|
||||
skill_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: 本地风控\ndescription: 本地说明\nversion: 1.0.0\n---\n# 本地风控\n\n本地正文\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
await gateway._handle_get_skill_detail(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager", "skill_name": "local_guard"},
|
||||
)
|
||||
|
||||
assert websocket.messages[-1]["type"] == "skill_detail_loaded"
|
||||
assert websocket.messages[-1]["agent_id"] == "risk_manager"
|
||||
assert websocket.messages[-1]["skill"]["source"] == "local"
|
||||
assert websocket.messages[-1]["skill"]["content"] == "# 本地风控\n\n本地正文"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_update_agent_skill_persists_and_returns_refresh(monkeypatch, tmp_path):
|
||||
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "extra_guard"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: extra_guard\ndescription: desc\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
async def _noop_reload():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
|
||||
|
||||
await gateway._handle_update_agent_skill(
|
||||
websocket,
|
||||
{
|
||||
"agent_id": "risk_manager",
|
||||
"skill_name": "extra_guard",
|
||||
"enabled": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_skill_updated"
|
||||
assert websocket.messages[-1]["type"] == "agent_skills_loaded"
|
||||
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
|
||||
assert "extra_guard" in agent_yaml.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_create_and_update_agent_local_skill(monkeypatch, tmp_path):
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
async def _noop_reload():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
|
||||
|
||||
await gateway._handle_create_agent_local_skill(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager", "skill_name": "local_guard"},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_local_skill_created"
|
||||
assert websocket.messages[1]["type"] == "agent_skills_loaded"
|
||||
assert websocket.messages[2]["type"] == "skill_detail_loaded"
|
||||
target = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard" / "SKILL.md"
|
||||
assert target.exists()
|
||||
|
||||
websocket.messages.clear()
|
||||
await gateway._handle_update_agent_local_skill(
|
||||
websocket,
|
||||
{
|
||||
"agent_id": "risk_manager",
|
||||
"skill_name": "local_guard",
|
||||
"content": "---\nname: 本地风控\ndescription: 更新后\nversion: 1.0.0\n---\n# 本地风控\n\n更新正文\n",
|
||||
},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_local_skill_updated"
|
||||
assert websocket.messages[1]["type"] == "skill_detail_loaded"
|
||||
assert "更新正文" in target.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_delete_agent_local_skill(monkeypatch, tmp_path):
|
||||
skill_dir = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "skills" / "local" / "local_guard"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: 本地风控\ndescription: desc\nversion: 1.0.0\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
|
||||
agent_yaml.parent.mkdir(parents=True, exist_ok=True)
|
||||
agent_yaml.write_text(
|
||||
"enabled_skills:\n"
|
||||
" - local_guard\n"
|
||||
"disabled_skills:\n"
|
||||
" - local_guard\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
async def _noop_reload():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
|
||||
|
||||
await gateway._handle_delete_agent_local_skill(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager", "skill_name": "local_guard"},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_local_skill_deleted"
|
||||
assert websocket.messages[1]["type"] == "agent_skills_loaded"
|
||||
assert not skill_dir.exists()
|
||||
assert "local_guard" not in agent_yaml.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_remove_agent_skill_marks_disabled(monkeypatch, tmp_path):
|
||||
skill_dir = tmp_path / "backend" / "skills" / "builtin" / "risk_review"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\nname: 风险审查\ndescription: desc\nversion: 1.0.0\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
async def _noop_reload():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
|
||||
|
||||
await gateway._handle_remove_agent_skill(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager", "skill_name": "risk_review"},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_skill_removed"
|
||||
assert websocket.messages[1]["type"] == "agent_skills_loaded"
|
||||
agent_yaml = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "agent.yaml"
|
||||
assert "risk_review" in agent_yaml.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_get_agent_workspace_file_returns_content(tmp_path):
|
||||
file_path = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "SOUL.md"
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text("soul content", encoding="utf-8")
|
||||
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
await gateway._handle_get_agent_workspace_file(
|
||||
websocket,
|
||||
{"agent_id": "risk_manager", "filename": "SOUL.md"},
|
||||
)
|
||||
|
||||
assert websocket.messages[-1] == {
|
||||
"type": "agent_workspace_file_loaded",
|
||||
"config_name": "demo",
|
||||
"agent_id": "risk_manager",
|
||||
"filename": "SOUL.md",
|
||||
"content": "soul content",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_update_agent_workspace_file_persists_and_returns_refresh(monkeypatch, tmp_path):
|
||||
gateway = make_gateway()
|
||||
gateway.config["config_name"] = "demo"
|
||||
gateway._project_root = tmp_path
|
||||
websocket = DummyWebSocket()
|
||||
|
||||
async def _noop_reload():
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(gateway, "_handle_reload_runtime_assets", _noop_reload)
|
||||
|
||||
await gateway._handle_update_agent_workspace_file(
|
||||
websocket,
|
||||
{
|
||||
"agent_id": "risk_manager",
|
||||
"filename": "SOUL.md",
|
||||
"content": "updated soul",
|
||||
},
|
||||
)
|
||||
|
||||
assert websocket.messages[0]["type"] == "agent_workspace_file_updated"
|
||||
assert websocket.messages[-1]["type"] == "agent_workspace_file_loaded"
|
||||
target = tmp_path / "runs" / "demo" / "agents" / "risk_manager" / "SOUL.md"
|
||||
assert target.read_text(encoding="utf-8") == "updated soul"
|
||||
|
||||
72
backend/tests/test_skills_cli.py
Normal file
72
backend/tests/test_skills_cli.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from backend import cli
|
||||
from backend.agents.skill_metadata import parse_skill_metadata
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
|
||||
|
||||
def test_parse_skill_metadata_extended_frontmatter(tmp_path):
|
||||
skill_dir = tmp_path / "demo_skill"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
"---\n"
|
||||
"name: demo_skill\n"
|
||||
"description: Demo description\n"
|
||||
"tools:\n"
|
||||
" - technical\n"
|
||||
"---\n\n"
|
||||
"# Demo Skill\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
parsed = parse_skill_metadata(skill_dir, source="builtin")
|
||||
|
||||
assert parsed.skill_name == "demo_skill"
|
||||
assert parsed.description == "Demo description"
|
||||
assert parsed.tools == ["technical"]
|
||||
|
||||
|
||||
def test_update_agent_skill_overrides(tmp_path):
|
||||
manager = SkillsManager(project_root=tmp_path)
|
||||
asset_dir = manager.get_agent_asset_dir("demo", "risk_manager")
|
||||
asset_dir.mkdir(parents=True, exist_ok=True)
|
||||
(asset_dir / "agent.yaml").write_text(
|
||||
"enabled_skills:\n"
|
||||
" - risk_review\n"
|
||||
"disabled_skills:\n"
|
||||
" - old_skill\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = manager.update_agent_skill_overrides(
|
||||
config_name="demo",
|
||||
agent_id="risk_manager",
|
||||
enable=["extra_guard"],
|
||||
disable=["risk_review"],
|
||||
)
|
||||
|
||||
assert result["enabled_skills"] == ["extra_guard"]
|
||||
assert result["disabled_skills"] == ["old_skill", "risk_review"]
|
||||
|
||||
|
||||
def test_skills_enable_disable_and_list(monkeypatch, tmp_path):
|
||||
builtin_root = tmp_path / "backend" / "skills" / "builtin"
|
||||
for name in ("risk_review", "extra_guard"):
|
||||
skill_dir = builtin_root / name
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text(
|
||||
f"---\nname: {name}\ndescription: {name} desc\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
printed = []
|
||||
monkeypatch.setattr(cli, "get_project_root", lambda: tmp_path)
|
||||
monkeypatch.setattr(cli.console, "print", lambda value: printed.append(value))
|
||||
|
||||
cli.skills_enable(agent_id="risk_manager", skill="extra_guard", config_name="demo")
|
||||
cli.skills_disable(agent_id="risk_manager", skill="risk_review", config_name="demo")
|
||||
cli.skills_list(config_name="demo", agent_id="risk_manager")
|
||||
|
||||
text_dump = "\n".join(str(item) for item in printed)
|
||||
assert "Enabled" in text_dump
|
||||
assert "Disabled" in text_dump
|
||||
assert any(getattr(item, "title", None) == "Skill Catalog" for item in printed)
|
||||
106
backend/tests/test_valuation_scripts.py
Normal file
106
backend/tests/test_valuation_scripts.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from backend.agents.skills_manager import SkillsManager
|
||||
from backend.skills.builtin.valuation_review.scripts.dcf_report import (
|
||||
build_dcf_report,
|
||||
)
|
||||
from backend.skills.builtin.valuation_review.scripts.multiple_valuation_report import (
|
||||
build_ev_ebitda_report,
|
||||
build_residual_income_report,
|
||||
)
|
||||
from backend.skills.builtin.valuation_review.scripts.owner_earnings_report import (
|
||||
build_owner_earnings_report,
|
||||
)
|
||||
|
||||
|
||||
def test_build_dcf_report_renders_assessment():
|
||||
report = build_dcf_report(
|
||||
[
|
||||
{
|
||||
"ticker": "AAPL",
|
||||
"current_fcf": 100.0,
|
||||
"growth_rate": 0.05,
|
||||
"market_cap": 900.0,
|
||||
"discount_rate": 0.10,
|
||||
"terminal_growth": 0.03,
|
||||
"num_years": 5,
|
||||
},
|
||||
],
|
||||
"2026-03-17",
|
||||
)
|
||||
|
||||
assert "DCF Valuation Analysis (2026-03-17)" in report
|
||||
assert "AAPL:" in report
|
||||
assert "Market Cap: $900" in report
|
||||
assert "Value Gap:" in report
|
||||
|
||||
|
||||
def test_build_owner_earnings_report_handles_errors():
|
||||
report = build_owner_earnings_report(
|
||||
[
|
||||
{
|
||||
"ticker": "MSFT",
|
||||
"error": "Negative owner earnings ($-50)",
|
||||
},
|
||||
],
|
||||
"2026-03-17",
|
||||
)
|
||||
|
||||
assert "MSFT: Negative owner earnings ($-50)" in report
|
||||
|
||||
|
||||
def test_multiple_valuation_reports_render_expected_sections():
|
||||
ev_report = build_ev_ebitda_report(
|
||||
[
|
||||
{
|
||||
"ticker": "NVDA",
|
||||
"current_multiple": 18.0,
|
||||
"median_multiple": 20.0,
|
||||
"current_ebitda": 50.0,
|
||||
"market_cap": 800.0,
|
||||
"net_debt": 100.0,
|
||||
},
|
||||
],
|
||||
"2026-03-17",
|
||||
)
|
||||
residual_report = build_residual_income_report(
|
||||
[
|
||||
{
|
||||
"ticker": "META",
|
||||
"book_value": 200.0,
|
||||
"initial_ri": 30.0,
|
||||
"market_cap": 300.0,
|
||||
"cost_of_equity": 0.10,
|
||||
"bv_growth": 0.03,
|
||||
"terminal_growth": 0.03,
|
||||
"num_years": 5,
|
||||
"margin_of_safety": 0.20,
|
||||
},
|
||||
],
|
||||
"2026-03-17",
|
||||
)
|
||||
|
||||
assert "EV/EBITDA Valuation (2026-03-17)" in ev_report
|
||||
assert "NVDA:" in ev_report
|
||||
assert "Residual Income Valuation (2026-03-17)" in residual_report
|
||||
assert "META:" in residual_report
|
||||
|
||||
|
||||
def test_prepare_active_skills_copies_skill_scripts(tmp_path):
|
||||
builtin_skill = tmp_path / "backend" / "skills" / "builtin" / "valuation_review"
|
||||
scripts_dir = builtin_skill / "scripts"
|
||||
scripts_dir.mkdir(parents=True, exist_ok=True)
|
||||
(builtin_skill / "SKILL.md").write_text(
|
||||
"---\nname: 估值分析\ndescription: desc\nversion: 1.0.0\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(scripts_dir / "dcf_report.py").write_text("print('ok')\n", encoding="utf-8")
|
||||
|
||||
manager = SkillsManager(project_root=tmp_path)
|
||||
active_map = manager.prepare_active_skills(
|
||||
config_name="demo",
|
||||
agent_defaults={"valuation_analyst": ["valuation_review"]},
|
||||
)
|
||||
|
||||
active_dir = active_map["valuation_analyst"][0]
|
||||
assert (active_dir / "scripts" / "dcf_report.py").exists()
|
||||
Reference in New Issue
Block a user