From dcdebe366aa5493e8252b2c981d51bea91de19b1 Mon Sep 17 00:00:00 2001 From: Huyvux12 <152865675+Huyvux12@users.noreply.github.com> Date: Wed, 11 Nov 2026 06:00:01 +0700 Subject: [PATCH] fix(module): report filter and subtitle modules as available without probing (#7357) --- app/locales/en-US.json | 1 + app/locales/zh-CN.json | 1 + app/locales/zh-TW.json | 1 + app/modules/__init__.py | 3 +- app/modules/filter/__init__.py | 8 ++- app/modules/subtitle/__init__.py | 8 ++- tests/test_filter_module_health.py | 104 +++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 tests/test_filter_module_health.py diff --git a/app/locales/en-US.json b/app/locales/en-US.json index 8cd87b77b..14b106c84 100644 --- a/app/locales/en-US.json +++ b/app/locales/en-US.json @@ -184,6 +184,7 @@ "分类策略转换失败": "Classification policy conversion failed", "不支持的分类策略模板": "Unsupported classification policy template", "模块不支持测试": "Module does not support testing", + "模块无需探测": "Module does not require probing", "网络请求失败": "Network request failed", "TMDB请求失败": "TMDB request failed", "无效的媒体类型": "Invalid media type", diff --git a/app/locales/zh-CN.json b/app/locales/zh-CN.json index 5a22c23ea..15a36b085 100644 --- a/app/locales/zh-CN.json +++ b/app/locales/zh-CN.json @@ -175,6 +175,7 @@ "media_source 和 media_id 必须同时提供": "media_source 和 media_id 必须同时提供", "不支持的分类策略模板": "不支持的分类策略模板", "模块不支持测试": "模块不支持测试", + "模块无需探测": "模块无需探测", "网络请求失败": "网络请求失败", "TMDB请求失败": "TMDB请求失败", "无效的媒体类型": "无效的媒体类型", diff --git a/app/locales/zh-TW.json b/app/locales/zh-TW.json index 94361f21d..66edaf284 100644 --- a/app/locales/zh-TW.json +++ b/app/locales/zh-TW.json @@ -175,6 +175,7 @@ "media_source 和 media_id 必须同时提供": "media_source 和 media_id 必須同時提供", "不支持的分类策略模板": "不支援的分類策略範本", "模块不支持测试": "模組不支援測試", + "模块无需探测": "模組無需探測", "网络请求失败": "網路請求失敗", "TMDB请求失败": "TMDB 請求失敗", "无效的媒体类型": "無效的媒體類型", diff --git a/app/modules/__init__.py b/app/modules/__init__.py index fdbc22094..8c2c4f997 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -129,7 +129,8 @@ class _ModuleBase(ConfigReloadMixin, metaclass=ABCMeta): """ 检查模块配置或外部服务是否可用。 - :return: 是否通过及失败说明;模块未启用或不提供检查时返回 None + :return: 是否通过及失败说明;模块未启用时返回 None;不提供独立检查的 + 已运行模块应返回成功结果并说明无需探测,而不是返回 None """ pass diff --git a/app/modules/filter/__init__.py b/app/modules/filter/__init__.py index 42101bda4..89c3823b8 100644 --- a/app/modules/filter/__init__.py +++ b/app/modules/filter/__init__.py @@ -147,11 +147,13 @@ class FilterModule(_ModuleBase): """ clear_rust_parse_options_cache() - def test(self) -> None: + def test(self) -> Tuple[bool, str]: """ - 保留模块自检入口;过滤器不依赖外部服务,无需执行探测。 + 过滤器不依赖外部服务,无需执行探测;模块运行即表示可用。 + + :return: 成功结果与无需探测的说明 """ - return None + return True, "模块无需探测" def init_setting(self) -> Optional[Tuple[str, Union[str, bool]]]: """ diff --git a/app/modules/subtitle/__init__.py b/app/modules/subtitle/__init__.py index 612a4a126..3983105f9 100644 --- a/app/modules/subtitle/__init__.py +++ b/app/modules/subtitle/__init__.py @@ -91,11 +91,13 @@ class SubtitleModule(_ModuleBase): """ pass - def test(self) -> Optional[Tuple[bool, str]]: + def test(self) -> Tuple[bool, str]: """ - 字幕能力依赖具体资源详情页,不提供独立连接测试。 + 字幕能力依赖具体资源详情页,不提供独立连接测试;模块运行即表示可用。 + + :return: 成功结果与无需探测的说明 """ - pass + return True, "模块无需探测" @classmethod def __normalize_subtitle_link(cls, page_url: str, sublink: str) -> Optional[str]: diff --git a/tests/test_filter_module_health.py b/tests/test_filter_module_health.py new file mode 100644 index 000000000..174370b98 --- /dev/null +++ b/tests/test_filter_module_health.py @@ -0,0 +1,104 @@ +""" +过滤器模块健康检查不应把“无需探测”报成“未启用”。 + +复现 #7354:运行中的 FilterModule 因 test() 返回 None,被 +ModuleManager.test() 按 falsy 结果处理成 (False, ""),健康检查接口 +据此返回 success=false,前端显示“未启用”。 +""" + +import pytest + +from app.api.endpoints import module as system_endpoint +from app.modules.filter import FilterModule +from app.runtime.extensions.module.manager import ModuleManager + + +def test_filter_module_reports_available_without_probing() -> None: + """ + 运行中的过滤器不依赖外部服务、无需探测,健康检查应返回成功。 + """ + manager = ModuleManager() + assert manager.get_running_module("FilterModule") is not None + assert manager.test("FilterModule") == (True, "模块无需探测") + + +def test_filter_module_test_returns_success_directly() -> None: + """ + 过滤器自检应直接返回成功结果,而不是表示“无结论”的 None。 + """ + assert FilterModule().test() == (True, "模块无需探测") + + +def test_subtitle_module_reports_available_without_probing() -> None: + """ + 字幕模块与过滤器同属“不提供独立检查”的已运行模块,应同样返回成功。 + """ + from app.modules.subtitle import SubtitleModule + + manager = ModuleManager() + assert manager.get_running_module("SubtitleModule") is not None + assert SubtitleModule().test() == (True, "模块无需探测") + assert manager.test("SubtitleModule") == (True, "模块无需探测") + + +def test_unselected_backend_module_keeps_old_contract() -> None: + """ + 未选中对应后端的已运行模块(如未使用 PostgreSQL)仍保持旧合同。 + + 这正是修复放在模块层而不是管理器层的原因:管理器无法区分“无需探测” + 与“后端未选用”两种 None,统一改成成功会把未选用的后端误报为可用。 + """ + manager = ModuleManager() + assert manager.get_running_module("PostgreSQLModule") is not None + assert manager.test("PostgreSQLModule") == (False, "") + + +def test_moduletest_endpoint_reports_filter_module_available() -> None: + """ + 健康检查接口对 FilterModule 应返回 success=true。 + """ + response = system_endpoint.moduletest("FilterModule", _="token") + assert response.success is True + assert response.message == "模块无需探测" + + +def test_moduletest_endpoint_localizes_no_probing_message() -> None: + """ + 英文请求下健康检查接口应返回翻译后的无需探测说明。 + """ + from app.runtime.localization import LocaleHelper + + token = LocaleHelper.set_current_locale("en-US") + try: + response = system_endpoint.moduletest("FilterModule", _="token") + finally: + LocaleHelper.reset_current_locale(token) + assert response.success is True + assert response.message == "Module does not require probing" + + +def test_not_running_module_keeps_old_contract() -> None: + """ + 未运行的模块仍保持旧合同,返回 (False, "")。 + """ + manager = ModuleManager() + assert manager.get_running_module("NavidromeModule") is None + assert manager.test("NavidromeModule") == (False, "") + + +def test_real_failure_result_still_fails(monkeypatch: pytest.MonkeyPatch) -> None: + """ + 模块 test() 真实返回失败时,管理器应原样透传失败结果。 + """ + manager = ModuleManager() + monkeypatch.setattr(FilterModule, "test", lambda self: (False, "连接失败")) + assert manager.test("FilterModule") == (False, "连接失败") + + +def test_real_success_result_still_passes_through(monkeypatch: pytest.MonkeyPatch) -> None: + """ + 模块 test() 真实返回成功时,管理器应原样透传成功结果。 + """ + manager = ModuleManager() + monkeypatch.setattr(FilterModule, "test", lambda self: (True, "")) + assert manager.test("FilterModule") == (True, "")