mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor: unify agent data port access
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.agent.tools.impl.add_subscribe import AddSubscribeTool
|
||||
@@ -28,8 +29,10 @@ class TestAgentAddSubscribeTool(unittest.TestCase):
|
||||
"app.agent.tools.impl.add_subscribe.SubscribeChain.async_add",
|
||||
new=AsyncMock(return_value=(1, "")),
|
||||
) as async_add, patch(
|
||||
"app.agent.tools.impl.add_subscribe.UserOper.get_name",
|
||||
return_value="moviepilot-user",
|
||||
"app.agent.tools.impl.add_subscribe.get_agent_user_port",
|
||||
return_value=SimpleNamespace(
|
||||
get_name=lambda **_kwargs: "moviepilot-user"
|
||||
),
|
||||
):
|
||||
result = asyncio.run(
|
||||
tool.run(
|
||||
@@ -55,8 +58,8 @@ class TestAgentAddSubscribeTool(unittest.TestCase):
|
||||
"app.agent.tools.impl.add_subscribe.SubscribeChain.async_add",
|
||||
new=AsyncMock(return_value=(1, "")),
|
||||
) as async_add, patch(
|
||||
"app.agent.tools.impl.add_subscribe.UserOper.get_name",
|
||||
return_value=None,
|
||||
"app.agent.tools.impl.add_subscribe.get_agent_user_port",
|
||||
return_value=SimpleNamespace(get_name=lambda **_kwargs: None),
|
||||
):
|
||||
result = asyncio.run(
|
||||
tool.run(
|
||||
@@ -81,8 +84,8 @@ class TestAgentAddSubscribeTool(unittest.TestCase):
|
||||
"app.agent.tools.impl.add_subscribe.SubscribeChain.async_add",
|
||||
new=AsyncMock(return_value=(1, "")),
|
||||
) as async_add, patch(
|
||||
"app.agent.tools.impl.add_subscribe.UserOper.get_name",
|
||||
return_value=None,
|
||||
"app.agent.tools.impl.add_subscribe.get_agent_user_port",
|
||||
return_value=SimpleNamespace(get_name=lambda **_kwargs: None),
|
||||
):
|
||||
result = asyncio.run(
|
||||
tool.run(
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Agent 命名数据端口工厂测试。"""
|
||||
|
||||
from app.application import agentdata
|
||||
|
||||
|
||||
def test_named_agent_data_getters_use_registered_factories(monkeypatch) -> None:
|
||||
"""每个命名 getter 都应创建组合根登记的对应端口实例。"""
|
||||
names = {
|
||||
"agent_chat": agentdata.get_agent_chat_port,
|
||||
"agent_task": agentdata.get_agent_task_port,
|
||||
"user": agentdata.get_agent_user_port,
|
||||
"site": agentdata.get_agent_site_port,
|
||||
"subscribe": agentdata.get_agent_subscribe_port,
|
||||
"subscribe_history": agentdata.get_agent_subscribe_history_port,
|
||||
"transfer_history": agentdata.get_agent_transfer_history_port,
|
||||
"download_history": agentdata.get_agent_download_history_port,
|
||||
"workflow": agentdata.get_agent_workflow_port,
|
||||
"plugin_data": agentdata.get_agent_plugin_data_port,
|
||||
}
|
||||
factories = {
|
||||
name: (lambda current=name: current)
|
||||
for name in names
|
||||
}
|
||||
monkeypatch.setattr(agentdata, "_ports", agentdata.AgentDataPorts(**factories))
|
||||
|
||||
assert {name: getter() for name, getter in names.items()} == {
|
||||
name: name for name in names
|
||||
}
|
||||
@@ -428,8 +428,10 @@ def test_add_download_preserves_album_context_and_full_coverage_marker():
|
||||
"_async_resolve_cached_context",
|
||||
new=AsyncMock(return_value=cached_context),
|
||||
), patch(
|
||||
"app.agent.tools.impl.add_download_tasks.SiteOper.async_get_by_name",
|
||||
new=AsyncMock(return_value=site),
|
||||
"app.agent.tools.impl.add_download_tasks.get_agent_site_port",
|
||||
return_value=SimpleNamespace(
|
||||
async_get_by_name=AsyncMock(return_value=site)
|
||||
),
|
||||
), patch.object(
|
||||
AddDownloadTasksTool,
|
||||
"_download_single_sync",
|
||||
@@ -479,7 +481,7 @@ def test_query_subscribe_history_uses_database_media_values_and_music_fields(mon
|
||||
return [record] if mtype == MediaType.MUSIC.value else []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.query_subscribe_history.SubscribeHistoryOper",
|
||||
"app.agent.tools.impl.query_subscribe_history.get_agent_subscribe_history_port",
|
||||
FakeHistoryOper,
|
||||
)
|
||||
tool = QuerySubscribeHistoryTool(session_id="session-1", user_id="10001")
|
||||
@@ -513,7 +515,7 @@ def test_music_history_filter_excludes_video_records(monkeypatch):
|
||||
return [movie_record] if mtype == MediaType.MOVIE.value else []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.query_subscribe_history.SubscribeHistoryOper",
|
||||
"app.agent.tools.impl.query_subscribe_history.get_agent_subscribe_history_port",
|
||||
FakeHistoryOper,
|
||||
)
|
||||
tool = QuerySubscribeHistoryTool(session_id="session-1", user_id="10001")
|
||||
|
||||
@@ -422,7 +422,7 @@ def test_query_plugin_data_truncates_large_payload() -> None:
|
||||
return_value=_plugin_snapshot(),
|
||||
),
|
||||
patch(
|
||||
"app.agent.tools.impl.query_plugin_data.PluginDataOper",
|
||||
"app.agent.tools.impl.query_plugin_data.get_agent_plugin_data_port",
|
||||
return_value=plugin_data_oper,
|
||||
),
|
||||
):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.agent.tools.impl.query_subscribes import QuerySubscribesTool
|
||||
from app.db.models.subscribe import Subscribe
|
||||
@@ -22,8 +23,10 @@ def test_agent_query_subscribes_returns_manual_total_episode():
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_subscribes.SubscribeOper.async_list",
|
||||
return_value=[subscribe],
|
||||
"app.agent.tools.impl.query_subscribes.get_agent_subscribe_port",
|
||||
return_value=SimpleNamespace(
|
||||
async_list=AsyncMock(return_value=[subscribe])
|
||||
),
|
||||
):
|
||||
result = asyncio.run(
|
||||
QuerySubscribesTool(session_id="session-1", user_id="10001").run(
|
||||
|
||||
@@ -28,7 +28,7 @@ class TestQueryWorkflowsTool(unittest.TestCase):
|
||||
workflow_oper.async_list = AsyncMock(return_value=[workflow])
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_workflows.WorkflowOper",
|
||||
"app.agent.tools.impl.query_workflows.get_agent_workflow_port",
|
||||
return_value=workflow_oper,
|
||||
):
|
||||
result = asyncio.run(tool.run())
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_query_sites_hides_only_sensitive_fields_for_non_admin_user():
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_sites.SiteOper"
|
||||
"app.agent.tools.impl.query_sites.get_agent_site_port"
|
||||
) as site_oper:
|
||||
site_oper.return_value.async_list = AsyncMock(return_value=[site])
|
||||
result = asyncio.run(tool.run())
|
||||
@@ -177,7 +177,7 @@ def test_query_sites_keeps_full_fields_for_admin_context():
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.agent.tools.impl.query_sites.SiteOper"
|
||||
"app.agent.tools.impl.query_sites.get_agent_site_port"
|
||||
) as site_oper:
|
||||
site_oper.return_value.async_list = AsyncMock(return_value=[site])
|
||||
result = asyncio.run(tool.run())
|
||||
@@ -369,7 +369,7 @@ def test_channel_agent_admin_user_id_does_not_bypass_user_lookup():
|
||||
username="normal-user",
|
||||
)
|
||||
|
||||
with patch("app.agent.orchestrator.UserOper") as user_oper:
|
||||
with patch("app.agent.orchestrator.get_agent_user_port") as user_oper:
|
||||
user_oper.return_value.async_get_by_name.return_value = SimpleNamespace(
|
||||
is_superuser=False
|
||||
)
|
||||
@@ -391,7 +391,7 @@ def test_channel_agent_rejects_local_admin_username_without_trusted_principal():
|
||||
)
|
||||
agent.is_channel_admin = False
|
||||
|
||||
with patch("app.agent.orchestrator.UserOper") as user_oper:
|
||||
with patch("app.agent.orchestrator.get_agent_user_port") as user_oper:
|
||||
user_oper.return_value.async_get_by_name = AsyncMock(
|
||||
return_value=SimpleNamespace(is_superuser=True)
|
||||
)
|
||||
@@ -414,7 +414,7 @@ def test_channel_agent_accepts_trusted_admin_principal_without_local_user():
|
||||
)
|
||||
agent.is_channel_admin = True
|
||||
|
||||
with patch("app.agent.orchestrator.UserOper") as user_oper:
|
||||
with patch("app.agent.orchestrator.get_agent_user_port") as user_oper:
|
||||
context = asyncio.run(
|
||||
agent._build_tool_context(should_dispatch_reply=True)
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_search_subscribe_uses_async_data_port(monkeypatch) -> None:
|
||||
raise AssertionError("async 工具不应调用同步订阅更新")
|
||||
|
||||
port = _AsyncSubscribePort()
|
||||
monkeypatch.setattr(search_subscribe_module, "SubscribeOper", lambda: port)
|
||||
monkeypatch.setattr(search_subscribe_module, "get_agent_subscribe_port", lambda: port)
|
||||
|
||||
async def _run_blocking(*_args, **_kwargs):
|
||||
await asyncio.sleep(0)
|
||||
@@ -79,7 +79,7 @@ def test_search_subscribe_rejects_paused_subscription_without_search(monkeypatch
|
||||
async_update = AsyncMock()
|
||||
|
||||
port = _AsyncSubscribePort()
|
||||
monkeypatch.setattr(search_subscribe_module, "SubscribeOper", lambda: port)
|
||||
monkeypatch.setattr(search_subscribe_module, "get_agent_subscribe_port", lambda: port)
|
||||
run_blocking = AsyncMock()
|
||||
monkeypatch.setattr(SearchSubscribeTool, "run_blocking", run_blocking)
|
||||
|
||||
|
||||
@@ -568,6 +568,37 @@ def test_transfer_chains_use_explicit_data_port_getters():
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_agent_consumers_use_explicit_data_port_getters():
|
||||
"""Agent 生产模块不得把兼容数据端口代理重新伪装成数据库 Oper。"""
|
||||
forbidden = {
|
||||
"AgentChatPort",
|
||||
"AgentTaskPort",
|
||||
"DownloadHistoryPort",
|
||||
"PluginDataPort",
|
||||
"SitePort",
|
||||
"SubscribeHistoryPort",
|
||||
"SubscribePort",
|
||||
"TransferHistoryPort",
|
||||
"UserPort",
|
||||
"WorkflowPort",
|
||||
}
|
||||
violations: list[str] = []
|
||||
for path in (APP_ROOT / "agent").rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.ImportFrom):
|
||||
continue
|
||||
if node.module != "app.application.agentdata":
|
||||
continue
|
||||
for alias in node.names:
|
||||
if alias.name in forbidden:
|
||||
violations.append(
|
||||
f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}:{alias.name}"
|
||||
)
|
||||
|
||||
assert violations == []
|
||||
|
||||
|
||||
def test_plugin_components_do_not_reexport_legacy_abi_names():
|
||||
"""新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。"""
|
||||
violations: list[str] = []
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_delete_transfer_history_tool_removes_old_dest_file_before_history(monke
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -96,7 +96,7 @@ def test_delete_transfer_history_tool_keeps_history_when_old_dest_delete_fails(m
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -150,7 +150,7 @@ def test_delete_transfer_history_tool_deletes_history_when_old_dest_is_missing(m
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -205,7 +205,7 @@ def test_delete_transfer_history_tool_keeps_successful_move_dest_as_reorganize_s
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -258,7 +258,7 @@ def test_delete_transfer_history_tool_only_treats_exact_move_as_reorganize_sourc
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -313,7 +313,7 @@ def test_delete_transfer_history_storage_work_runs_outside_event_loop(monkeypatc
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
@@ -371,7 +371,7 @@ def test_delete_transfer_history_cancellation_keeps_history_record(monkeypatch):
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
|
||||
"app.agent.tools.impl.delete_transfer_history.get_agent_transfer_history_port",
|
||||
FakeTransferHistoryOper,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
|
||||
Reference in New Issue
Block a user