From 554c83a5b4e9c7a2b70b5ad91e15ae87578b02a8 Mon Sep 17 00:00:00 2001 From: supreme0597 Date: Fri, 15 May 2026 18:31:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(skill):=20=E4=BF=AE=E5=A4=8D=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E6=8A=80=E8=83=BD=E6=97=B6=E9=85=8D=E7=BD=AE=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E8=B7=AF=E5=BE=84=E9=94=99=E8=AF=AF=E5=8F=8A?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正 AgentConfigRepository 技能写入路径,确保技能保存至 config_json->context->skills - 修复 test_install_skill 单元测试的各种 mock 与断言,解决由于接口变更和依赖不匹配导致的失败 --- .../repositories/agent_config_repository.py | 11 +++-- .../toolkits/buildin/test_install_skill.py | 47 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/backend/package/yuxi/repositories/agent_config_repository.py b/backend/package/yuxi/repositories/agent_config_repository.py index b28d9aeb..f5dd10fc 100644 --- a/backend/package/yuxi/repositories/agent_config_repository.py +++ b/backend/package/yuxi/repositories/agent_config_repository.py @@ -242,9 +242,14 @@ class AgentConfigRepository: sql = text(""" UPDATE agent_configs SET config_json = jsonb_set( - CAST(config_json AS jsonb), - '{skills}', - COALESCE(CAST(config_json->'skills' AS jsonb), CAST('[]' AS jsonb)) || CAST(:new_slugs_json AS jsonb), + COALESCE(CAST(config_json AS jsonb), '{}'::jsonb), + '{context}', + jsonb_set( + COALESCE(CAST(config_json->'context' AS jsonb), '{}'::jsonb), + '{skills}', + COALESCE(CAST(config_json->'context'->'skills' AS jsonb), CAST('[]' AS jsonb)) || CAST(:new_slugs_json AS jsonb), + true + ), true ) WHERE id = :id diff --git a/backend/test/unit/agents/toolkits/buildin/test_install_skill.py b/backend/test/unit/agents/toolkits/buildin/test_install_skill.py index 63f4e25b..54acb908 100644 --- a/backend/test/unit/agents/toolkits/buildin/test_install_skill.py +++ b/backend/test/unit/agents/toolkits/buildin/test_install_skill.py @@ -110,11 +110,11 @@ async def test_assert_admin_missing_user_raises_error(): with patch("yuxi.agents.toolkits.buildin.install_skill.UserRepository") as mock_repo_cls: mock_repo = MagicMock() - mock_repo.get_by_user_id = AsyncMock(return_value=mock_user) + mock_repo.get_by_id_with_db = AsyncMock(return_value=mock_user) mock_repo_cls.return_value = mock_repo with pytest.raises(ValueError, match="用户不存在"): - await _assert_admin("non-existent-user") + await _assert_admin(mock_session, "1") @pytest.mark.asyncio @@ -128,11 +128,11 @@ async def test_assert_admin_non_admin_raises_error(): with patch("yuxi.agents.toolkits.buildin.install_skill.UserRepository") as mock_repo_cls: mock_repo = MagicMock() - mock_repo.get_by_user_id = AsyncMock(return_value=mock_user) + mock_repo.get_by_id_with_db = AsyncMock(return_value=mock_user) mock_repo_cls.return_value = mock_repo with pytest.raises(ValueError, match="仅管理员可以安装 skill"): - await _assert_admin("test-user") + await _assert_admin(mock_session, "1") @pytest.mark.asyncio @@ -146,11 +146,11 @@ async def test_assert_admin_admin_passes(): with patch("yuxi.agents.toolkits.buildin.install_skill.UserRepository") as mock_repo_cls: mock_repo = MagicMock() - mock_repo.get_by_user_id = AsyncMock(return_value=mock_user) + mock_repo.get_by_id_with_db = AsyncMock(return_value=mock_user) mock_repo_cls.return_value = mock_repo # Should not raise - await _assert_admin("test-admin-user") + await _assert_admin(mock_session, "1") @pytest.mark.asyncio @@ -164,11 +164,11 @@ async def test_assert_admin_superadmin_passes(): with patch("yuxi.agents.toolkits.buildin.install_skill.UserRepository") as mock_repo_cls: mock_repo = MagicMock() - mock_repo.get_by_user_id = AsyncMock(return_value=mock_user) + mock_repo.get_by_id_with_db = AsyncMock(return_value=mock_user) mock_repo_cls.return_value = mock_repo # Should not raise - await _assert_admin("test-superadmin-user") + await _assert_admin(mock_session, "1") # ============================================================================= @@ -227,7 +227,8 @@ async def test_install_skill_no_context_returns_error(): @pytest.mark.asyncio -async def test_install_skill_git_no_skill_names_returns_error(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_git_no_skill_names_returns_error(mock_pg): """install_skill should return error Command when using Git source without skill_names.""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -252,7 +253,8 @@ async def test_install_skill_git_no_skill_names_returns_error(): @pytest.mark.asyncio -async def test_install_skill_git_with_skill_names_passes_admin_check(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_git_with_skill_names_passes_admin_check(mock_pg): """install_skill with Git source and skill_names should pass admin check (but may fail other steps).""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -263,7 +265,7 @@ async def test_install_skill_git_with_skill_names_passes_admin_check(): # Mock _assert_admin to not raise mock_assert.return_value = None - with patch("yuxi.agents.toolkits.buildin.install_skill._install_git_skills") as mock_install: + with patch("yuxi.services.remote_skill_install_service.install_remote_skills_batch") as mock_install: # Mock the git installation to return success mock_install.return_value = [{"slug": "test-skill", "success": True}] @@ -285,7 +287,8 @@ async def test_install_skill_git_with_skill_names_passes_admin_check(): @pytest.mark.asyncio -async def test_install_skill_sandbox_success(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_sandbox_success(mock_pg): """install_skill with valid sandbox path should work (full mock).""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -296,7 +299,7 @@ async def test_install_skill_sandbox_success(): mock_assert.return_value = None with patch("yuxi.agents.toolkits.buildin.install_skill._install_skill_from_sandbox") as mock_install: - mock_install.return_value = "my-skill" + mock_install.return_value = ("my-skill", False) with patch("yuxi.agents.toolkits.buildin.install_skill._enable_skill_in_current_config") as mock_enable: mock_enable.return_value = True @@ -314,7 +317,8 @@ async def test_install_skill_sandbox_success(): @pytest.mark.asyncio -async def test_install_skill_value_error_handling(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_value_error_handling(mock_pg): """install_skill should handle ValueError from admin check gracefully.""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -338,7 +342,8 @@ async def test_install_skill_value_error_handling(): @pytest.mark.asyncio -async def test_install_skill_exception_handling(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_exception_handling(mock_pg): """install_skill should handle unexpected exceptions gracefully.""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -362,7 +367,8 @@ async def test_install_skill_exception_handling(): @pytest.mark.asyncio -async def test_install_skill_partial_config_failure(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_partial_config_failure(mock_pg): """install_skill should handle partial config persistence failure.""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -373,7 +379,7 @@ async def test_install_skill_partial_config_failure(): mock_assert.return_value = None with patch("yuxi.agents.toolkits.buildin.install_skill._install_skill_from_sandbox") as mock_install: - mock_install.return_value = "my-skill" + mock_install.return_value = ("my-skill", False) with patch("yuxi.agents.toolkits.buildin.install_skill._enable_skill_in_current_config") as mock_enable: # Simulate config persistence failure @@ -389,11 +395,12 @@ async def test_install_skill_partial_config_failure(): assert isinstance(result, Command) result_str = str(result.update) # Should indicate config persistence issue - assert "持久化" in result_str or "Skill 已安装" in result_str + assert "当前会话配置中激活失败" in result_str or "技能已安装到系统" in result_str @pytest.mark.asyncio -async def test_install_skill_slug_warning_for_renamed(): +@patch("yuxi.agents.toolkits.buildin.install_skill.pg_manager") +async def test_install_skill_slug_warning_for_renamed(mock_pg): """install_skill should include warning when skill is renamed.""" runtime = MagicMock() runtime.context.thread_id = "test-thread-id" @@ -405,7 +412,7 @@ async def test_install_skill_slug_warning_for_renamed(): with patch("yuxi.agents.toolkits.buildin.install_skill._install_skill_from_sandbox") as mock_install: # Simulate skill being renamed during installation - mock_install.return_value = "my-skill-v2" + mock_install.return_value = ("my-skill-v2", True) with patch("yuxi.agents.toolkits.buildin.install_skill._enable_skill_in_current_config") as mock_enable: mock_enable.return_value = True