refactor: expand chain runtime config snapshot

This commit is contained in:
jxxghp
2026-08-22 13:21:52 +08:00
parent 941c167084
commit fa2bf1fe4d
13 changed files with 78 additions and 50 deletions
@@ -9,7 +9,7 @@
"root": "app"
},
"settings_imports": {
"count": 166,
"count": 161,
"files": [
"app/adapters/cache/backends.py",
"app/adapters/cache/redis.py",
@@ -69,14 +69,10 @@
"app/application/security/token.py",
"app/application/security/url.py",
"app/application/torrent.py",
"app/chain/_messaging.py",
"app/chain/_recognition.py",
"app/chain/_transfer.py",
"app/chain/download.py",
"app/chain/interaction.py",
"app/chain/media.py",
"app/chain/message.py",
"app/chain/recommend.py",
"app/chain/scraping.py",
"app/chain/search.py",
"app/chain/site.py",
@@ -84,7 +80,6 @@
"app/chain/system.py",
"app/chain/torrents.py",
"app/chain/transfer.py",
"app/chain/user.py",
"app/cli.py",
"app/db/base.py",
"app/db/engine.py",
+2 -6
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6364,
"edge_sha256": "0f241a46ed27309c2071d4c99f00ba067ad02b3974581cc206ddec4c522a982c",
"edge_count": 6360,
"edge_sha256": "09c923d8b167a889e22829c320e05f8801c62400f672b1386d6524c55b064564",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -2851,7 +2851,6 @@
"app.chain._messaging -> app.foundation",
"app.chain._messaging -> app.foundation.identity",
"app.chain._messaging -> app.runtime",
"app.chain._messaging -> app.runtime.config",
"app.chain._messaging -> app.runtime.extensions",
"app.chain._messaging -> app.runtime.extensions.service_config",
"app.chain._messaging -> app.runtime.log",
@@ -2891,7 +2890,6 @@
"app.chain._recognition -> app.domain.meta.metamusic",
"app.chain._recognition -> app.runtime",
"app.chain._recognition -> app.runtime.cache",
"app.chain._recognition -> app.runtime.config",
"app.chain._recognition -> app.runtime.events",
"app.chain._recognition -> app.runtime.log",
"app.chain._recognition -> app.schemas",
@@ -3015,7 +3013,6 @@
"app.chain.interaction -> app.foundation",
"app.chain.interaction -> app.foundation.url",
"app.chain.interaction -> app.runtime",
"app.chain.interaction -> app.runtime.config",
"app.chain.interaction -> app.runtime.log",
"app.chain.interaction -> app.schemas",
"app.chain.interaction -> app.schemas.download",
@@ -3366,7 +3363,6 @@
"app.chain.user -> app.application.security.token",
"app.chain.user -> app.chain",
"app.chain.user -> app.runtime",
"app.chain.user -> app.runtime.config",
"app.chain.user -> app.runtime.log",
"app.chain.user -> app.schemas",
"app.chain.user -> app.schemas.event",
+19 -2
View File
@@ -1,8 +1,11 @@
"""配置快照与窄读写端口测试。"""
import asyncio
from dataclasses import FrozenInstanceError
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.application.configuration import (
ApiRuntimeConfig,
ChainRuntimeConfig,
@@ -78,5 +81,19 @@ def test_api_runtime_provider_returns_frozen_snapshot_per_request() -> None:
assert before_reload.ai_agent_enable is False
assert after_reload.ai_agent_enable is True
configure_runtime_configuration,
get_api_runtime_config_snapshot,
def test_chain_runtime_config_is_an_instance_scoped_frozen_snapshot() -> None:
"""Chain 配置应随实例固定,避免同一业务调用中途读取到 reload 后的新值。"""
snapshot = ChainRuntimeConfig(
media_extensions=(".mkv",),
superuser="root",
media_recognize_share=True,
resource_url="https://example.test/#/resource",
)
assert snapshot.superuser == "root"
assert snapshot.media_recognize_share is True
assert snapshot.resource_url == "https://example.test/#/resource"
with pytest.raises(FrozenInstanceError):
snapshot.superuser = "changed" # type: ignore[misc]
+18 -9
View File
@@ -4,6 +4,7 @@
共享识别成功后回填本地缓存、音乐识别上报/查询载荷,以及命中缓存不重复上报等场景。
"""
import asyncio
from dataclasses import replace
from unittest.mock import AsyncMock, Mock, patch
from app.chain import ChainBase
@@ -41,6 +42,14 @@ def _tmdb_media(
)
def _enable_media_recognize_share(chain: ChainBase) -> None:
"""为单个链实例启用共享识别配置快照。"""
chain.runtime_config = replace(
chain.runtime_config,
media_recognize_share=True,
)
def test_report_shared_result_after_local_recognize_success():
"""本地识别成功后应上报共享识别结果。"""
chain = ChainBase()
@@ -64,6 +73,7 @@ def test_report_shared_result_after_local_recognize_success():
def test_query_shared_result_when_local_recognize_failed():
"""本地识别失败后应回查共享识别结果,并按共享ID再次识别。"""
chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试剧集")
shared_media = _tmdb_media("测试剧集", 200, MediaType.TV, year="2024")
@@ -109,6 +119,7 @@ def test_query_shared_result_when_local_recognize_failed():
def test_async_query_shared_result_when_local_recognize_failed():
"""异步识别失败后也应回查共享识别结果。"""
chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试异步剧集")
shared_media = _tmdb_media("测试异步剧集", 300, MediaType.TV, year="2025")
async_run_module = AsyncMock(side_effect=[None, shared_media])
@@ -157,6 +168,7 @@ def test_async_query_shared_result_when_local_recognize_failed():
def test_backfill_local_cache_after_shared_recognize_success():
"""共享识别后二次本地识别成功时,应回填原始名称对应的本地识别缓存。"""
chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("测试缓存回填", MediaType.MOVIE)
shared_media = MediaInfo(
media_source=MediaSource.TMDB,
@@ -304,6 +316,7 @@ def test_report_shared_result_with_distinct_keyword_meta():
def test_query_shared_result_with_distinct_keyword_meta():
"""本地识别失败后应按辅助前名称回查共享结果。"""
chain = ChainBase()
_enable_media_recognize_share(chain)
meta = _build_meta("辅助识别后的名称", MediaType.TV)
meta.year = "2024"
share_meta = _build_meta("辅助识别前的名称", MediaType.UNKNOWN)
@@ -594,6 +607,7 @@ def test_chain_recognize_media_reports_music_share_result():
def test_chain_recognize_media_queries_music_share_when_local_failed():
"""音乐本地识别失败后应回查共享识别并按数据源原生 ID 二次识别。"""
chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"])
music = _music_info()
@@ -641,6 +655,7 @@ def test_chain_recognize_media_queries_music_share_when_local_failed():
def test_chain_recognize_media_queries_music_share_after_local_fallback():
"""本地标签兜底没有远端身份时,仍应通过共享结果补成标准音乐身份。"""
chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"])
fallback = MusicInfo(title="晴天", artists=["周杰伦"])
music = _music_info()
@@ -669,9 +684,6 @@ def test_chain_recognize_media_queries_music_share_after_local_fallback():
), patch.object(
chain,
"_update_local_recognize_cache",
), patch(
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE",
True,
):
result = chain.recognize_media(meta=meta, cache=False)
@@ -687,6 +699,7 @@ def test_chain_recognize_media_queries_music_share_after_local_fallback():
def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
"""异步音乐识别也必须在返回本地兜底前尝试共享身份补全。"""
chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="晴天", artists=["周杰伦"])
fallback = MusicInfo(title="晴天", artists=["周杰伦"])
music = _music_info()
@@ -717,9 +730,6 @@ def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
chain,
"_async_update_local_recognize_cache",
new=AsyncMock(),
), patch(
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE",
True,
):
result = await chain.async_recognize_media(meta=meta, cache=False)
return result, query_share, recognize_source
@@ -745,6 +755,7 @@ def test_chain_async_recognize_media_queries_music_share_after_local_fallback():
def test_chain_recognize_media_skips_music_report_for_fallback_result():
"""共享也未命中时保留音乐标签兜底,且不把无身份结果上报。"""
chain = MediaChain()
_enable_media_recognize_share(chain)
meta = MetaMusic(title="未知曲目", artists=["未知艺术家"])
fallback = MusicInfo(title="未知曲目", artists=["未知艺术家"])
@@ -753,9 +764,7 @@ def test_chain_recognize_media_skips_music_report_for_fallback_result():
return_value=None,
) as query_mock, patch(
"app.chain._recognition.MoviePilotServerHelper.report_recognize_share"
) as report_mock, patch(
"app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True
):
) as report_mock:
result = chain.recognize_media(meta=meta, cache=False)
assert result is fallback
@@ -41,6 +41,7 @@ def _bare_chain() -> ChainBase:
"""构造不执行初始化的识别链实例,并挂上无插件响应的事件管理器桩。"""
chain = object.__new__(ChainBase)
chain.eventmanager = Mock(check=Mock(return_value=False))
chain.runtime_config = SimpleNamespace(media_recognize_share=True)
return chain
@@ -57,7 +58,6 @@ def test_sync_shared_recognize_success_increments_persisted_count(monkeypatch):
type=MediaType.MOVIE,
)
increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, media]))
monkeypatch.setattr(chain, "_update_local_recognize_cache", Mock())
monkeypatch.setattr(
@@ -86,7 +86,6 @@ def test_sync_shared_result_without_local_match_does_not_increment(monkeypatch):
chain = _bare_chain()
meta = _build_meta("共享识别失败电影")
increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(chain, "run_module", Mock(side_effect=[None, None]))
monkeypatch.setattr(
MoviePilotServerHelper,
@@ -122,7 +121,6 @@ def test_async_shared_recognize_success_increments_persisted_count(monkeypatch):
type=MediaType.MOVIE,
)
increment = _mock_counter(monkeypatch)
monkeypatch.setattr("app.chain._recognition.settings.MEDIA_RECOGNIZE_SHARE", True)
monkeypatch.setattr(
chain,
"async_run_module",