157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
"""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
|