"""CLI skill learn-risk-guards 命令单元测试""" from unittest.mock import AsyncMock, MagicMock, patch from typer.testing import CliRunner from agentkit.evolution.risk_guard_learner import RiskGuardSuggestion runner = CliRunner() def _make_suggestion( skill_name="code_reviewer", precondition="需要代码输入", confidence=0.8, reason="避免空输入" ): return RiskGuardSuggestion( skill_name=skill_name, precondition=precondition, confidence=confidence, reason=reason, source_experience_ids=["e1", "e2"], ) class TestLearnRiskGuardsCommand: def test_renders_suggestions_with_human_review_notice(self): """learn() 返回 2 条建议 → 输出含 Rich 表格 + '人工审查' 提示""" from agentkit.cli.main import app mock_learner = MagicMock() mock_learner.learn = AsyncMock( return_value=[_make_suggestion(), _make_suggestion("monitor", "需要网络", 0.6)] ) with patch("agentkit.cli.skill._build_risk_guard_learner", return_value=mock_learner): result = runner.invoke(app, ["skill", "learn-risk-guards"]) assert result.exit_code == 0 assert "人工审查" in result.stdout assert "code_reviewer" in result.stdout assert "monitor" in result.stdout assert "需要代码输入" in result.stdout def test_empty_suggestions_message(self): """learn() 返回空 → 输出'未从失败轨迹中学习到风险守卫建议'""" from agentkit.cli.main import app mock_learner = MagicMock() mock_learner.learn = AsyncMock(return_value=[]) with patch("agentkit.cli.skill._build_risk_guard_learner", return_value=mock_learner): result = runner.invoke(app, ["skill", "learn-risk-guards"]) assert result.exit_code == 0 assert "未从失败轨迹中学习到风险守卫建议" in result.stdout def test_learner_build_failure_exits_nonzero(self): """_build_risk_guard_learner 返回 None → 非零退出码""" from agentkit.cli.main import app with patch("agentkit.cli.skill._build_risk_guard_learner", return_value=None): result = runner.invoke(app, ["skill", "learn-risk-guards"]) assert result.exit_code == 1 def test_skill_option_passed_to_learn(self): """--skill 参数透传给 learn(skill_name=...)""" from agentkit.cli.main import app mock_learner = MagicMock() mock_learner.learn = AsyncMock(return_value=[]) with patch("agentkit.cli.skill._build_risk_guard_learner", return_value=mock_learner): result = runner.invoke(app, ["skill", "learn-risk-guards", "--skill", "code_reviewer"]) assert result.exit_code == 0 mock_learner.learn.assert_called_once_with(skill_name="code_reviewer", top_k=20) def test_top_k_option_passed_to_learn(self): from agentkit.cli.main import app mock_learner = MagicMock() mock_learner.learn = AsyncMock(return_value=[]) with patch("agentkit.cli.skill._build_risk_guard_learner", return_value=mock_learner): result = runner.invoke(app, ["skill", "learn-risk-guards", "--top-k", "50"]) assert result.exit_code == 0 mock_learner.learn.assert_called_once_with(skill_name=None, top_k=50) def test_server_url_not_supported(self): """--server-url 远程模式暂不支持""" from agentkit.cli.main import app result = runner.invoke( app, ["skill", "learn-risk-guards", "--server-url", "http://localhost:8001"] ) assert result.exit_code == 1 class TestBuildRiskGuardLearnerErrorPaths: """测试 _build_risk_guard_learner 的真实错误路径(不 mock 函数本身)""" def test_no_config_file_returns_none(self): """find_config_path 返回 None → 打印错误 + 返回 None""" from agentkit.cli import skill as skill_module with patch("agentkit.server.config.find_config_path", return_value=None): result = skill_module._build_risk_guard_learner() assert result is None def test_no_database_url_returns_none(self): """server_config 无 database_url → 返回 None""" from agentkit.cli import skill as skill_module mock_config = MagicMock() mock_config.evolution = {} mock_config.memory = {} with ( patch("agentkit.server.config.find_config_path", return_value="/fake/path.yaml"), patch("agentkit.server.config.load_config_with_dotenv", return_value=mock_config), patch("agentkit.cli.chat._build_gateway", return_value=MagicMock()), patch.dict("os.environ", {}, clear=False), ): # Ensure DATABASE_URL is not set import os old = os.environ.pop("DATABASE_URL", None) try: result = skill_module._build_risk_guard_learner() finally: if old is not None: os.environ["DATABASE_URL"] = old assert result is None def test_try_get_experience_store_no_database_url(self): """_try_get_experience_store 无 database_url → 返回 None""" from agentkit.cli import skill as skill_module mock_config = MagicMock() mock_config.evolution = {} mock_config.memory = {"episodic": {}} with patch.dict("os.environ", {}, clear=False): import os old = os.environ.pop("DATABASE_URL", None) try: result = skill_module._try_get_experience_store(mock_config) finally: if old is not None: os.environ["DATABASE_URL"] = old assert result is None def test_try_get_experience_store_with_database_url(self): """_try_get_experience_store 有 database_url → 构建 ExperienceStore""" from agentkit.cli import skill as skill_module mock_config = MagicMock() mock_config.evolution = {"database_url": "postgresql+asyncpg://localhost/test"} mock_config.memory = {} with patch( "agentkit.memory.models.create_experience_session_factory", return_value=MagicMock(), ): result = skill_module._try_get_experience_store(mock_config) assert result is not None