mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-31 13:07:56 +08:00
refactor: separate semantic architecture baselines
This commit is contained in:
@@ -250,8 +250,11 @@ def collect_dependency_baseline() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def collect_run_module_contracts() -> dict[str, Any]:
|
||||
"""收集字符串模块调度方法及其同步、异步调用位置。"""
|
||||
def _collect_run_module_locations() -> tuple[
|
||||
dict[str, list[dict[str, Any]]],
|
||||
list[dict[str, Any]],
|
||||
]:
|
||||
"""扫描模块调度调用及其当前源码位置。"""
|
||||
calls: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
dynamic_calls: list[dict[str, Any]] = []
|
||||
for module_name, path in discover_modules().items():
|
||||
@@ -274,18 +277,53 @@ def collect_run_module_contracts() -> dict[str, Any]:
|
||||
calls[node.args[0].value].append(location)
|
||||
else:
|
||||
dynamic_calls.append(location)
|
||||
return calls, dynamic_calls
|
||||
|
||||
|
||||
def _aggregate_locations(
|
||||
locations: list[dict[str, Any]],
|
||||
fields: tuple[str, ...],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""按稳定字段聚合调用位置,排除行号但保留调用次数。"""
|
||||
counts: dict[tuple[Any, ...], int] = defaultdict(int)
|
||||
for location in locations:
|
||||
counts[tuple(location[field] for field in fields)] += 1
|
||||
return [
|
||||
{
|
||||
**dict(zip(fields, values)),
|
||||
"count": count,
|
||||
}
|
||||
for values, count in sorted(counts.items())
|
||||
]
|
||||
|
||||
|
||||
def collect_run_module_contracts() -> dict[str, Any]:
|
||||
"""收集不受源码行号变化影响的模块调度语义契约。"""
|
||||
calls, dynamic_calls = _collect_run_module_locations()
|
||||
stable_calls = {
|
||||
method: sorted(
|
||||
locations,
|
||||
key=lambda item: (item["caller"], item["line"], item["mode"]),
|
||||
)
|
||||
method: _aggregate_locations(locations, ("caller", "mode"))
|
||||
for method, locations in sorted(calls.items())
|
||||
}
|
||||
return {
|
||||
"method_count": len(stable_calls),
|
||||
"call_count": sum(len(locations) for locations in stable_calls.values()),
|
||||
"call_count": sum(len(locations) for locations in calls.values()),
|
||||
"dynamic_call_count": len(dynamic_calls),
|
||||
"methods": stable_calls,
|
||||
"dynamic_calls": _aggregate_locations(dynamic_calls, ("caller", "mode")),
|
||||
}
|
||||
|
||||
|
||||
def collect_run_module_diagnostics() -> dict[str, Any]:
|
||||
"""收集模块调度调用的当前源码位置,仅用于人工诊断。"""
|
||||
calls, dynamic_calls = _collect_run_module_locations()
|
||||
return {
|
||||
"methods": {
|
||||
method: sorted(
|
||||
locations,
|
||||
key=lambda item: (item["caller"], item["line"], item["mode"]),
|
||||
)
|
||||
for method, locations in sorted(calls.items())
|
||||
},
|
||||
"dynamic_calls": sorted(
|
||||
dynamic_calls,
|
||||
key=lambda item: (item["caller"], item["line"], item["mode"]),
|
||||
@@ -330,8 +368,14 @@ def _event_enum_members(enum_name: str) -> tuple[str, ...]:
|
||||
)
|
||||
|
||||
|
||||
def collect_event_contracts() -> dict[str, Any]:
|
||||
"""收集宿主事件枚举的生产者、消费者和动态调用位置。"""
|
||||
def _collect_event_locations() -> tuple[
|
||||
list[str],
|
||||
dict[str, list[dict[str, Any]]],
|
||||
dict[str, list[dict[str, Any]]],
|
||||
list[dict[str, Any]],
|
||||
list[dict[str, Any]],
|
||||
]:
|
||||
"""扫描事件枚举及其生产、消费位置,供语义和诊断视图复用。"""
|
||||
event_members = _event_enum_members("EventType")
|
||||
chain_event_members = _event_enum_members("ChainEventType")
|
||||
|
||||
@@ -390,28 +434,70 @@ def collect_event_contracts() -> dict[str, Any]:
|
||||
*(f"EventType.{member}" for member in event_members),
|
||||
*(f"ChainEventType.{member}" for member in chain_event_members),
|
||||
]
|
||||
return (
|
||||
enum_names,
|
||||
producers,
|
||||
consumers,
|
||||
dynamic_producers,
|
||||
dynamic_consumers,
|
||||
)
|
||||
|
||||
|
||||
def collect_event_contracts() -> dict[str, Any]:
|
||||
"""收集不受源码行号变化影响的宿主事件语义契约。"""
|
||||
(
|
||||
enum_names,
|
||||
producers,
|
||||
consumers,
|
||||
dynamic_producers,
|
||||
dynamic_consumers,
|
||||
) = _collect_event_locations()
|
||||
contracts = {
|
||||
name: {
|
||||
"producers": sorted(
|
||||
"producers": _aggregate_locations(
|
||||
producers.get(name, []),
|
||||
key=lambda item: (item["caller"], item["line"]),
|
||||
("caller",),
|
||||
),
|
||||
"consumers": sorted(
|
||||
"consumers": _aggregate_locations(
|
||||
consumers.get(name, []),
|
||||
key=lambda item: (item["caller"], item["line"]),
|
||||
("caller",),
|
||||
),
|
||||
}
|
||||
for name in sorted(enum_names)
|
||||
}
|
||||
return {
|
||||
"event_count": len(contracts),
|
||||
"producer_count": sum(
|
||||
len(item["producers"]) for item in contracts.values()
|
||||
),
|
||||
"consumer_count": sum(
|
||||
len(item["consumers"]) for item in contracts.values()
|
||||
),
|
||||
"producer_count": sum(len(items) for items in producers.values()),
|
||||
"consumer_count": sum(len(items) for items in consumers.values()),
|
||||
"events": contracts,
|
||||
"dynamic_producers": _aggregate_locations(dynamic_producers, ("caller",)),
|
||||
"dynamic_consumers": _aggregate_locations(dynamic_consumers, ("caller",)),
|
||||
}
|
||||
|
||||
|
||||
def collect_event_diagnostics() -> dict[str, Any]:
|
||||
"""收集事件生产与消费的当前源码位置,仅用于人工诊断。"""
|
||||
(
|
||||
enum_names,
|
||||
producers,
|
||||
consumers,
|
||||
dynamic_producers,
|
||||
dynamic_consumers,
|
||||
) = _collect_event_locations()
|
||||
return {
|
||||
"events": {
|
||||
name: {
|
||||
"producers": sorted(
|
||||
producers.get(name, []),
|
||||
key=lambda item: (item["caller"], item["line"]),
|
||||
),
|
||||
"consumers": sorted(
|
||||
consumers.get(name, []),
|
||||
key=lambda item: (item["caller"], item["line"]),
|
||||
),
|
||||
}
|
||||
for name in sorted(enum_names)
|
||||
},
|
||||
"dynamic_producers": sorted(
|
||||
dynamic_producers,
|
||||
key=lambda item: (item["caller"], item["line"]),
|
||||
@@ -503,7 +589,7 @@ def collect_compat_manifest() -> dict[str, Any]:
|
||||
def collect_runtime_baseline() -> dict[str, Any]:
|
||||
"""生成模块调度、SDK 和兼容层公开契约基线。"""
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"schema_version": 2,
|
||||
"run_module": collect_run_module_contracts(),
|
||||
"events": collect_event_contracts(),
|
||||
"sdk_exports": collect_sdk_exports(),
|
||||
@@ -511,6 +597,14 @@ def collect_runtime_baseline() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def collect_runtime_diagnostics() -> dict[str, Any]:
|
||||
"""生成带当前源码行号的运行契约诊断视图,不写入语义 fixture。"""
|
||||
return {
|
||||
"run_module": collect_run_module_diagnostics(),
|
||||
"events": collect_event_diagnostics(),
|
||||
}
|
||||
|
||||
|
||||
def git_head(repository: Path) -> str:
|
||||
"""读取外部插件仓当前提交,失败时返回可诊断占位值。"""
|
||||
result = subprocess.run(
|
||||
@@ -633,11 +727,13 @@ def collect_official_plugin_baseline(plugin_repo: Path) -> dict[str, Any]:
|
||||
if hook in defined_names:
|
||||
hook_files[hook].add(relative)
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"source": {
|
||||
"schema_version": 3,
|
||||
"scope": {
|
||||
"repository": "MoviePilot-Plugins",
|
||||
"head": git_head(plugin_repo),
|
||||
"roots": [root.name for root in roots],
|
||||
},
|
||||
"provenance": {
|
||||
"head": git_head(plugin_repo),
|
||||
"python_file_count": len(paths),
|
||||
"source_sha256": digest.hexdigest(),
|
||||
},
|
||||
@@ -669,12 +765,103 @@ def write_json(path: Path, value: dict[str, Any]) -> None:
|
||||
|
||||
|
||||
def _display_path(path: Path) -> Path:
|
||||
"""优先返回仓库相对路径,便于 CLI 输出稳定可读。"""
|
||||
try:
|
||||
return path.relative_to(PROJECT_ROOT)
|
||||
except ValueError:
|
||||
return path
|
||||
|
||||
|
||||
def _migrate_runtime_baseline(value: dict[str, Any]) -> dict[str, Any]:
|
||||
"""把包含源码行号的 v1 运行契约转换为稳定语义结构。"""
|
||||
if value.get("schema_version") != 1:
|
||||
return value
|
||||
run_module = value["run_module"]
|
||||
events = value["events"]
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"run_module": {
|
||||
**{
|
||||
key: run_module[key]
|
||||
for key in ("method_count", "call_count", "dynamic_call_count")
|
||||
},
|
||||
"methods": {
|
||||
method: _aggregate_locations(locations, ("caller", "mode"))
|
||||
for method, locations in run_module["methods"].items()
|
||||
},
|
||||
"dynamic_calls": _aggregate_locations(
|
||||
run_module["dynamic_calls"],
|
||||
("caller", "mode"),
|
||||
),
|
||||
},
|
||||
"events": {
|
||||
**{
|
||||
key: events[key]
|
||||
for key in ("event_count", "producer_count", "consumer_count")
|
||||
},
|
||||
"events": {
|
||||
event_name: {
|
||||
"producers": _aggregate_locations(
|
||||
contract["producers"],
|
||||
("caller",),
|
||||
),
|
||||
"consumers": _aggregate_locations(
|
||||
contract["consumers"],
|
||||
("caller",),
|
||||
),
|
||||
}
|
||||
for event_name, contract in events["events"].items()
|
||||
},
|
||||
"dynamic_producers": _aggregate_locations(
|
||||
events["dynamic_producers"],
|
||||
("caller",),
|
||||
),
|
||||
"dynamic_consumers": _aggregate_locations(
|
||||
events["dynamic_consumers"],
|
||||
("caller",),
|
||||
),
|
||||
},
|
||||
"sdk_exports": value["sdk_exports"],
|
||||
"compat_manifest": value["compat_manifest"],
|
||||
}
|
||||
|
||||
|
||||
def _migrate_plugin_baseline(value: dict[str, Any]) -> dict[str, Any]:
|
||||
"""把来源信息混排的 v2 插件基线转换为 scope/provenance 结构。"""
|
||||
if value.get("schema_version") != 2:
|
||||
return value
|
||||
source = value["source"]
|
||||
return {
|
||||
"schema_version": 3,
|
||||
"scope": {
|
||||
"repository": source["repository"],
|
||||
"roots": source["roots"],
|
||||
},
|
||||
"provenance": {
|
||||
"head": source["head"],
|
||||
"python_file_count": source["python_file_count"],
|
||||
"source_sha256": source["source_sha256"],
|
||||
},
|
||||
"imports": value["imports"],
|
||||
"hooks": value["hooks"],
|
||||
"api_routes": value["api_routes"],
|
||||
}
|
||||
|
||||
|
||||
def semantic_baseline(path: Path, value: dict[str, Any]) -> dict[str, Any]:
|
||||
"""返回参与门禁比较的稳定语义视图,并兼容读取旧 fixture。"""
|
||||
if path.name == RUNTIME_BASELINE_PATH.name:
|
||||
return _migrate_runtime_baseline(value)
|
||||
if path.name == PLUGIN_BASELINE_PATH.name:
|
||||
migrated = _migrate_plugin_baseline(value)
|
||||
return {
|
||||
key: item
|
||||
for key, item in migrated.items()
|
||||
if key != "provenance"
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def check_json(
|
||||
path: Path,
|
||||
actual: dict[str, Any],
|
||||
@@ -683,7 +870,14 @@ def check_json(
|
||||
) -> bool:
|
||||
"""比较当前扫描结果和已提交基线并输出限定范围的更新提示。"""
|
||||
expected = json.loads(path.read_text(encoding="utf-8"))
|
||||
if expected == actual:
|
||||
expected_semantic = semantic_baseline(path, expected)
|
||||
actual_semantic = semantic_baseline(path, actual)
|
||||
if expected_semantic == actual_semantic:
|
||||
if expected != actual:
|
||||
print(
|
||||
f"基线语义未变化,但 schema/provenance 已变化:{_display_path(path)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return True
|
||||
print(
|
||||
f"架构基线已变化:{_display_path(path)};"
|
||||
@@ -713,6 +907,11 @@ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
||||
type=Path,
|
||||
help="官方插件操作所需的独立 MoviePilot-Plugins 仓路径",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diagnostics",
|
||||
action="store_true",
|
||||
help="随宿主检查打印当前运行契约源码位置",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
if args.check or args.write:
|
||||
if not args.scope:
|
||||
@@ -724,11 +923,15 @@ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
||||
file=sys.stderr,
|
||||
)
|
||||
setattr(args, f"{'check' if args.check else 'write'}_{args.scope}", True)
|
||||
elif args.scope:
|
||||
parser.error("--scope 只能与旧 --check/--write 一起使用")
|
||||
plugin_action = args.check_plugins or args.write_plugins
|
||||
if plugin_action and not args.plugin_repo:
|
||||
parser.error("插件基线操作必须指定 --plugin-repo")
|
||||
if not plugin_action and args.plugin_repo:
|
||||
parser.error("--plugin-repo 只能用于插件基线操作")
|
||||
if args.diagnostics and not args.check_host:
|
||||
parser.error("--diagnostics 只能与 --check-host 一起使用")
|
||||
return args
|
||||
|
||||
|
||||
@@ -763,6 +966,8 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
check_json(path, baseline, write_hint=write_hint)
|
||||
for path, baseline in baselines
|
||||
]
|
||||
if args.diagnostics:
|
||||
print(json.dumps(collect_runtime_diagnostics(), ensure_ascii=False, indent=2))
|
||||
return 0 if all(checks) else 1
|
||||
|
||||
|
||||
|
||||
@@ -322,6 +322,7 @@ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
|
||||
|
||||
|
||||
def _display_path(path: Path) -> Path:
|
||||
"""优先返回仓库相对路径,便于 CLI 输出稳定可读。"""
|
||||
try:
|
||||
return path.relative_to(PROJECT_ROOT)
|
||||
except ValueError:
|
||||
|
||||
+161
-63
@@ -1111,48 +1111,59 @@
|
||||
"response_model": null
|
||||
}
|
||||
],
|
||||
"plugins.v2/chatgpt/__init__.py": [
|
||||
"plugins.v2/courseorganizer/__init__.py": [
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "get_cache_stats",
|
||||
"endpoint_return": "dict",
|
||||
"endpoint": "get_review_route",
|
||||
"endpoint_return": "Any",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"path": "/cache_stats",
|
||||
"path": "/review",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "clear_cache",
|
||||
"endpoint_return": "dict",
|
||||
"endpoint": "save_review_route",
|
||||
"endpoint_return": "Any",
|
||||
"methods": [
|
||||
"GET"
|
||||
"POST"
|
||||
],
|
||||
"path": "/clear_cache",
|
||||
"path": "/review",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "clear_usage_stats",
|
||||
"endpoint_return": "dict",
|
||||
"endpoint": "refresh_review_route",
|
||||
"endpoint_return": "Any",
|
||||
"methods": [
|
||||
"GET"
|
||||
"POST"
|
||||
],
|
||||
"path": "/clear_usage_stats",
|
||||
"path": "/review/refresh",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "get_usage_stats",
|
||||
"endpoint_return": "dict",
|
||||
"endpoint": "associate_tmdb_route",
|
||||
"endpoint_return": "Any",
|
||||
"methods": [
|
||||
"GET"
|
||||
"POST"
|
||||
],
|
||||
"path": "/usage_stats",
|
||||
"path": "/review/tmdb/associate",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "search_tmdb_route",
|
||||
"endpoint_return": "Any",
|
||||
"methods": [
|
||||
"POST"
|
||||
],
|
||||
"path": "/review/tmdb/search",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
}
|
||||
@@ -2501,6 +2512,52 @@
|
||||
"response_model": null
|
||||
}
|
||||
],
|
||||
"plugins.v3/chatgpt/__init__.py": [
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "get_cache_stats",
|
||||
"endpoint_return": "dict",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"path": "/cache_stats",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "clear_cache",
|
||||
"endpoint_return": "dict",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"path": "/clear_cache",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "clear_usage_stats",
|
||||
"endpoint_return": "dict",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"path": "/clear_usage_stats",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
},
|
||||
{
|
||||
"auth": "'bear'",
|
||||
"endpoint": "get_usage_stats",
|
||||
"endpoint_return": "dict",
|
||||
"methods": [
|
||||
"GET"
|
||||
],
|
||||
"path": "/usage_stats",
|
||||
"response_class": null,
|
||||
"response_model": null
|
||||
}
|
||||
],
|
||||
"plugins.v3/doubanrank/__init__.py": [
|
||||
{
|
||||
"auth": null,
|
||||
@@ -2638,7 +2695,7 @@
|
||||
]
|
||||
},
|
||||
"get_api": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -2656,6 +2713,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -2708,6 +2766,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -2728,7 +2787,7 @@
|
||||
"files": []
|
||||
},
|
||||
"get_command": {
|
||||
"file_count": 79,
|
||||
"file_count": 81,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -2745,6 +2804,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -2796,6 +2856,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -2827,7 +2888,7 @@
|
||||
]
|
||||
},
|
||||
"get_form": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -2845,6 +2906,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -2897,6 +2959,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -2923,7 +2986,7 @@
|
||||
]
|
||||
},
|
||||
"get_page": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -2941,6 +3004,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -2993,6 +3057,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -3009,13 +3074,14 @@
|
||||
]
|
||||
},
|
||||
"get_render_mode": {
|
||||
"file_count": 11,
|
||||
"file_count": 12,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
"plugins.v2/airecognizerenhancer/__init__.py",
|
||||
"plugins.v2/brushflow/__init__.py",
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/fullscreenposterwall/__init__.py",
|
||||
"plugins.v2/hrblocker/__init__.py",
|
||||
"plugins.v2/oidcauth/__init__.py",
|
||||
@@ -3025,7 +3091,7 @@
|
||||
]
|
||||
},
|
||||
"get_service": {
|
||||
"file_count": 47,
|
||||
"file_count": 48,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/autoauction/__init__.py",
|
||||
@@ -3035,6 +3101,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -3081,7 +3148,7 @@
|
||||
"files": []
|
||||
},
|
||||
"get_state": {
|
||||
"file_count": 82,
|
||||
"file_count": 85,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -3100,6 +3167,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -3152,6 +3220,8 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/chatgpt/openai.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -3168,7 +3238,7 @@
|
||||
]
|
||||
},
|
||||
"init_plugin": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -3186,6 +3256,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -3238,6 +3309,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -3254,7 +3326,7 @@
|
||||
]
|
||||
},
|
||||
"stop_service": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -3272,6 +3344,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -3324,6 +3397,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -3348,11 +3422,12 @@
|
||||
]
|
||||
},
|
||||
"app.agent.llm": {
|
||||
"file_count": 3,
|
||||
"file_count": 4,
|
||||
"files": [
|
||||
"plugins.v2/airecognizerenhancer/__init__.py",
|
||||
"plugins.v2/chatgpt/openai.py",
|
||||
"plugins.v2/qbuploadlimiter/__init__.py"
|
||||
"plugins.v2/courseorganizer/providers.py",
|
||||
"plugins.v2/qbuploadlimiter/__init__.py",
|
||||
"plugins.v3/chatgpt/openai.py"
|
||||
]
|
||||
},
|
||||
"app.agent.llm.helper": {
|
||||
@@ -3415,10 +3490,11 @@
|
||||
]
|
||||
},
|
||||
"app.chain.media": {
|
||||
"file_count": 9,
|
||||
"file_count": 10,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/services/hdhive_openapi.py",
|
||||
"plugins.v2/airecognizerenhancer/__init__.py",
|
||||
"plugins.v2/courseorganizer/providers.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
"plugins.v2/doubansync/__init__.py",
|
||||
"plugins.v2/lexiannot/__init__.py",
|
||||
@@ -3548,7 +3624,7 @@
|
||||
]
|
||||
},
|
||||
"app.core.config": {
|
||||
"file_count": 70,
|
||||
"file_count": 73,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agentresourceofficer/services/hdhive_openapi.py",
|
||||
@@ -3584,6 +3660,7 @@
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/contractcheck/siteuserinfo/__init__.py",
|
||||
"plugins.v2/courseorganizer/providers.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -3594,6 +3671,7 @@
|
||||
"plugins.v2/ffmpegthumb/__init__.py",
|
||||
"plugins.v2/fullscreenposterwall/__init__.py",
|
||||
"plugins.v2/imdbsource/__init__.py",
|
||||
"plugins.v2/imdbsource/imdbapi.py",
|
||||
"plugins.v2/imdbsource/imdbhelper.py",
|
||||
"plugins.v2/invitessignin/__init__.py",
|
||||
"plugins.v2/iyuuautoseed/__init__.py",
|
||||
@@ -3619,7 +3697,8 @@
|
||||
"plugins.v2/tvdbdiscover/__init__.py",
|
||||
"plugins.v2/tvfirstwatch/__init__.py",
|
||||
"plugins.v2/updatewechatip/__init__.py",
|
||||
"plugins.v2/wechatclawbot/__init__.py"
|
||||
"plugins.v2/wechatclawbot/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.core.context": {
|
||||
@@ -3645,7 +3724,7 @@
|
||||
]
|
||||
},
|
||||
"app.core.event": {
|
||||
"file_count": 40,
|
||||
"file_count": 41,
|
||||
"files": [
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
"plugins.v2/airecognizerenhancer/__init__.py",
|
||||
@@ -3686,7 +3765,8 @@
|
||||
"plugins.v2/tvfirstwatch/__init__.py",
|
||||
"plugins.v2/updatewechatip/__init__.py",
|
||||
"plugins.v2/wechatclawbot/__init__.py",
|
||||
"plugins.v2/wxpusher/__init__.py"
|
||||
"plugins.v2/wxpusher/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.core.meta": {
|
||||
@@ -3880,8 +3960,9 @@
|
||||
]
|
||||
},
|
||||
"app.db.user_oper": {
|
||||
"file_count": 2,
|
||||
"file_count": 3,
|
||||
"files": [
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/doubansync/__init__.py",
|
||||
"plugins.v2/oidcauth/__init__.py"
|
||||
]
|
||||
@@ -4036,7 +4117,7 @@
|
||||
]
|
||||
},
|
||||
"app.log": {
|
||||
"file_count": 97,
|
||||
"file_count": 99,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agentresourceofficer/feishu_channel.py",
|
||||
@@ -4082,6 +4163,7 @@
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/contractcheck/siteuserinfo/__init__.py",
|
||||
"plugins.v2/contractcheck/siteuserinfo/nexus_php.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -4134,7 +4216,8 @@
|
||||
"plugins.v2/updatewechatip/__init__.py",
|
||||
"plugins.v2/wechatclawbot/__init__.py",
|
||||
"plugins.v2/wechatclawbot/ilink/client.py",
|
||||
"plugins.v2/wxpusher/__init__.py"
|
||||
"plugins.v2/wxpusher/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.modules.bangumi": {
|
||||
@@ -4210,7 +4293,7 @@
|
||||
]
|
||||
},
|
||||
"app.plugins": {
|
||||
"file_count": 81,
|
||||
"file_count": 83,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -4228,6 +4311,7 @@
|
||||
"plugins.v2/clashruleprovider/__init__.py",
|
||||
"plugins.v2/cleaninvalidseed/__init__.py",
|
||||
"plugins.v2/contractcheck/__init__.py",
|
||||
"plugins.v2/courseorganizer/__init__.py",
|
||||
"plugins.v2/crossseed/__init__.py",
|
||||
"plugins.v2/dailysummary/__init__.py",
|
||||
"plugins.v2/doubanrank/__init__.py",
|
||||
@@ -4280,6 +4364,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
@@ -4322,9 +4407,10 @@
|
||||
]
|
||||
},
|
||||
"app.plugins.chatgpt.openai": {
|
||||
"file_count": 1,
|
||||
"file_count": 2,
|
||||
"files": [
|
||||
"plugins.v2/chatgpt/__init__.py"
|
||||
"plugins.v2/chatgpt/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.plugins.ffmpegthumb.ffmpeg_helper": {
|
||||
@@ -4334,24 +4420,21 @@
|
||||
]
|
||||
},
|
||||
"app.plugins.imdbsource.imdbhelper": {
|
||||
"file_count": 2,
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v2/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py"
|
||||
"plugins.v2/imdbsource/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.plugins.imdbsource.officialapi": {
|
||||
"file_count": 2,
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v2/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py"
|
||||
"plugins.v2/imdbsource/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.plugins.imdbsource.schema": {
|
||||
"file_count": 2,
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v2/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py"
|
||||
"plugins.v2/imdbsource/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.plugins.iyuuautoseed.iyuu_helper": {
|
||||
@@ -4385,7 +4468,7 @@
|
||||
]
|
||||
},
|
||||
"app.schemas": {
|
||||
"file_count": 48,
|
||||
"file_count": 50,
|
||||
"files": [
|
||||
"plugins.v2/autoclean/__init__.py",
|
||||
"plugins.v2/blurayremux/__init__.py",
|
||||
@@ -4426,9 +4509,11 @@
|
||||
"plugins.v2/wechatclawbot/__init__.py",
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubanrank/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/schema/imdbapi.py",
|
||||
"plugins.v3/libraryscraper/__init__.py",
|
||||
"plugins.v3/maoyanrank/__init__.py",
|
||||
"plugins.v3/mediaservermsg/__init__.py",
|
||||
@@ -4437,14 +4522,26 @@
|
||||
"plugins.v3/tvdbdiscover/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.schemas.context": {
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v3/imdbsource/schema/imdbapi.py"
|
||||
]
|
||||
},
|
||||
"app.schemas.event": {
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v2/hrblocker/__init__.py"
|
||||
]
|
||||
},
|
||||
"app.schemas.exception": {
|
||||
"file_count": 1,
|
||||
"files": [
|
||||
"plugins.v3/imdbsource/officialapi.py"
|
||||
]
|
||||
},
|
||||
"app.schemas.types": {
|
||||
"file_count": 65,
|
||||
"file_count": 66,
|
||||
"files": [
|
||||
"plugins.v2/agentresourceofficer/__init__.py",
|
||||
"plugins.v2/agenttokens/__init__.py",
|
||||
@@ -4500,6 +4597,7 @@
|
||||
"plugins.v3/autoclean/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
"plugins.v3/brushflow/__init__.py",
|
||||
"plugins.v3/chatgpt/__init__.py",
|
||||
"plugins.v3/doubansync/__init__.py",
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
"plugins.v3/episodegroupmeta/__init__.py",
|
||||
@@ -4522,9 +4620,8 @@
|
||||
]
|
||||
},
|
||||
"app.sdk.cache": {
|
||||
"file_count": 5,
|
||||
"file_count": 4,
|
||||
"files": [
|
||||
"plugins.v3/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/imdbapi.py",
|
||||
"plugins.v3/imdbsource/imdbhelper.py",
|
||||
"plugins.v3/imdbsource/officialapi.py",
|
||||
@@ -4532,7 +4629,7 @@
|
||||
]
|
||||
},
|
||||
"app.sdk.config": {
|
||||
"file_count": 20,
|
||||
"file_count": 21,
|
||||
"files": [
|
||||
"plugins.v3/agentresourceofficer/__init__.py",
|
||||
"plugins.v3/agentresourceofficer/services/hdhive_openapi.py",
|
||||
@@ -4546,6 +4643,7 @@
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
"plugins.v3/episodegroupmeta/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/imdbapi.py",
|
||||
"plugins.v3/imdbsource/imdbhelper.py",
|
||||
"plugins.v3/libraryscraper/__init__.py",
|
||||
"plugins.v3/maoyanrank/__init__.py",
|
||||
@@ -4573,7 +4671,7 @@
|
||||
]
|
||||
},
|
||||
"app.sdk.logging": {
|
||||
"file_count": 24,
|
||||
"file_count": 23,
|
||||
"files": [
|
||||
"plugins.v3/agentresourceofficer/__init__.py",
|
||||
"plugins.v3/agentresourceofficer/feishu_channel.py",
|
||||
@@ -4588,7 +4686,6 @@
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
"plugins.v3/episodegroupmeta/__init__.py",
|
||||
"plugins.v3/historytov2/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/imdbapi.py",
|
||||
"plugins.v3/imdbsource/imdbhelper.py",
|
||||
"plugins.v3/imdbsource/officialapi.py",
|
||||
@@ -4622,7 +4719,7 @@
|
||||
]
|
||||
},
|
||||
"app.sdk.network": {
|
||||
"file_count": 17,
|
||||
"file_count": 16,
|
||||
"files": [
|
||||
"plugins.v3/animeupscale/__init__.py",
|
||||
"plugins.v3/bangumicoll/__init__.py",
|
||||
@@ -4632,7 +4729,6 @@
|
||||
"plugins.v3/downloadsitetag/__init__.py",
|
||||
"plugins.v3/episodegroupmeta/__init__.py",
|
||||
"plugins.v3/historytov2/__init__.py",
|
||||
"plugins.v3/imdbsource/__init__.py",
|
||||
"plugins.v3/imdbsource/imdbapi.py",
|
||||
"plugins.v3/imdbsource/imdbhelper.py",
|
||||
"plugins.v3/imdbsource/officialapi.py",
|
||||
@@ -4839,15 +4935,17 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"schema_version": 2,
|
||||
"source": {
|
||||
"head": "ddb41dbcbbea21196154a7f6d5fdba3aa34a5e4a",
|
||||
"python_file_count": 231,
|
||||
"provenance": {
|
||||
"head": "217c8d25ffe6ff0b3f6352c4278fd6896def442e",
|
||||
"python_file_count": 237,
|
||||
"source_sha256": "c3796565ff3fd49a8ff3f1e1373d9afab44fd7b98371e44f662c32739fe53d15"
|
||||
},
|
||||
"schema_version": 3,
|
||||
"scope": {
|
||||
"repository": "MoviePilot-Plugins",
|
||||
"roots": [
|
||||
"plugins.v2",
|
||||
"plugins.v3"
|
||||
],
|
||||
"source_sha256": "63663538bd7f5b7c6a9f318ab58ad35d8c5ab68bd7d798d07a5dcdf600f062ad"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+341
-540
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,133 @@ def test_architecture_plugin_action_requires_repository(capsys):
|
||||
assert "必须指定 --plugin-repo" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_architecture_diagnostics_only_support_host_check(capsys):
|
||||
"""源码位置诊断不得与写操作或插件扫描混用。"""
|
||||
with pytest.raises(SystemExit) as error:
|
||||
architecture_baseline.parse_args(["--write-host", "--diagnostics"])
|
||||
|
||||
assert error.value.code == 2
|
||||
assert "只能与 --check-host" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_runtime_semantics_ignore_line_changes_but_keep_call_count(tmp_path: Path):
|
||||
"""旧 fixture 的行号变化不影响门禁,重复调用次数仍属于语义。"""
|
||||
baseline_path = tmp_path / "runtime-contract-baseline.json"
|
||||
old_value = {
|
||||
"schema_version": 1,
|
||||
"run_module": {
|
||||
"method_count": 1,
|
||||
"call_count": 2,
|
||||
"dynamic_call_count": 0,
|
||||
"methods": {
|
||||
"search": [
|
||||
{"caller": "app.chain.search", "line": 10, "mode": "sync"},
|
||||
{"caller": "app.chain.search", "line": 11, "mode": "sync"},
|
||||
]
|
||||
},
|
||||
"dynamic_calls": [],
|
||||
},
|
||||
"events": {
|
||||
"event_count": 1,
|
||||
"producer_count": 1,
|
||||
"consumer_count": 0,
|
||||
"events": {
|
||||
"EventType.NoticeMessage": {
|
||||
"producers": [{"caller": "app.chain.message", "line": 20}],
|
||||
"consumers": [],
|
||||
}
|
||||
},
|
||||
"dynamic_producers": [],
|
||||
"dynamic_consumers": [],
|
||||
},
|
||||
"sdk_exports": {},
|
||||
"compat_manifest": {},
|
||||
}
|
||||
moved_value = json.loads(json.dumps(old_value))
|
||||
moved_value["run_module"]["methods"]["search"][0]["line"] = 1000
|
||||
moved_value["run_module"]["methods"]["search"][1]["line"] = 1001
|
||||
moved_value["events"]["events"]["EventType.NoticeMessage"]["producers"][0][
|
||||
"line"
|
||||
] = 2000
|
||||
|
||||
old_semantic = architecture_baseline.semantic_baseline(baseline_path, old_value)
|
||||
moved_semantic = architecture_baseline.semantic_baseline(
|
||||
baseline_path,
|
||||
moved_value,
|
||||
)
|
||||
|
||||
assert old_semantic == moved_semantic
|
||||
assert old_semantic["run_module"]["methods"]["search"] == [
|
||||
{"caller": "app.chain.search", "mode": "sync", "count": 2}
|
||||
]
|
||||
|
||||
|
||||
def test_plugin_provenance_does_not_participate_in_semantic_gate(tmp_path: Path):
|
||||
"""插件仓 revision 和源码摘要变化不应伪装成 ABI 变化。"""
|
||||
baseline_path = tmp_path / "official-plugin-baseline.json"
|
||||
first = {
|
||||
"schema_version": 3,
|
||||
"scope": {"repository": "MoviePilot-Plugins", "roots": ["plugins.v3"]},
|
||||
"provenance": {
|
||||
"head": "a" * 40,
|
||||
"python_file_count": 1,
|
||||
"source_sha256": "a" * 64,
|
||||
},
|
||||
"imports": {},
|
||||
"hooks": {},
|
||||
"api_routes": {},
|
||||
}
|
||||
second = json.loads(json.dumps(first))
|
||||
second["provenance"] = {
|
||||
"head": "b" * 40,
|
||||
"python_file_count": 2,
|
||||
"source_sha256": "b" * 64,
|
||||
}
|
||||
|
||||
assert architecture_baseline.semantic_baseline(
|
||||
baseline_path,
|
||||
first,
|
||||
) == architecture_baseline.semantic_baseline(baseline_path, second)
|
||||
|
||||
|
||||
def test_plugin_v2_fixture_migrates_before_semantic_comparison(tmp_path: Path):
|
||||
"""旧插件 fixture 应在内存中迁移,不能因 schema 排列变化误报 ABI。"""
|
||||
baseline_path = tmp_path / "official-plugin-baseline.json"
|
||||
old_value = {
|
||||
"schema_version": 2,
|
||||
"source": {
|
||||
"repository": "MoviePilot-Plugins",
|
||||
"head": "a" * 40,
|
||||
"roots": ["plugins.v2", "plugins.v3"],
|
||||
"python_file_count": 1,
|
||||
"source_sha256": "a" * 64,
|
||||
},
|
||||
"imports": {"app.sdk.logging": {"file_count": 1, "files": ["x.py"]}},
|
||||
"hooks": {},
|
||||
"api_routes": {},
|
||||
}
|
||||
new_value = {
|
||||
"schema_version": 3,
|
||||
"scope": {
|
||||
"repository": "MoviePilot-Plugins",
|
||||
"roots": ["plugins.v2", "plugins.v3"],
|
||||
},
|
||||
"provenance": {
|
||||
"head": "b" * 40,
|
||||
"python_file_count": 2,
|
||||
"source_sha256": "b" * 64,
|
||||
},
|
||||
"imports": old_value["imports"],
|
||||
"hooks": {},
|
||||
"api_routes": {},
|
||||
}
|
||||
|
||||
assert architecture_baseline.semantic_baseline(
|
||||
baseline_path,
|
||||
old_value,
|
||||
) == architecture_baseline.semantic_baseline(baseline_path, new_value)
|
||||
|
||||
|
||||
def test_architecture_write_host_only_updates_host_files(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
|
||||
@@ -41,9 +41,10 @@ def test_official_plugin_baseline_records_external_source():
|
||||
baseline_path = BASELINE_ROOT / "official-plugin-baseline.json"
|
||||
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert baseline["source"]["repository"] == "MoviePilot-Plugins"
|
||||
assert len(baseline["source"]["head"]) == 40
|
||||
assert baseline["source"]["roots"] == ["plugins.v2", "plugins.v3"]
|
||||
assert baseline["schema_version"] == 3
|
||||
assert baseline["scope"]["repository"] == "MoviePilot-Plugins"
|
||||
assert baseline["scope"]["roots"] == ["plugins.v2", "plugins.v3"]
|
||||
assert len(baseline["provenance"]["head"]) == 40
|
||||
assert all(
|
||||
not path.startswith("app/plugins/")
|
||||
for contract in (*baseline["imports"].values(), *baseline["hooks"].values())
|
||||
@@ -95,6 +96,15 @@ def test_startup_performance_baseline_records_all_cold_import_targets():
|
||||
assert contract["loaded_module_count"] > 0
|
||||
|
||||
|
||||
def test_runtime_contract_baseline_excludes_diagnostic_line_numbers():
|
||||
"""运行契约 fixture 只保存稳定语义,源码位置必须按需诊断。"""
|
||||
baseline_path = BASELINE_ROOT / "runtime-contract-baseline.json"
|
||||
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert baseline["schema_version"] == 2
|
||||
assert '"line"' not in json.dumps(baseline)
|
||||
|
||||
|
||||
def test_startup_performance_baseline_records_normal_and_safe_lifecycle_resources():
|
||||
"""非功能基线必须同时记录正常/安全模式和隔离资源增量。"""
|
||||
baseline_path = BASELINE_ROOT / "startup-performance-baseline.json"
|
||||
|
||||
Reference in New Issue
Block a user