"""Host Module Adapter 对 Capability Runtime 的兼容合同测试。""" from __future__ import annotations import importlib import os import subprocess import sys from pathlib import Path from types import SimpleNamespace from typing import Iterator from unittest.mock import Mock import pytest from app.db.oper.systemconfig import SystemConfigOper from app.foundation.singleton import Singleton from app.runtime.capabilities.errors import CapabilityRuntimeClosedError from app.runtime.capabilities.model import SelectorSchema from app.runtime.capabilities.registry import CapabilityRegistry from app.runtime.events import Event, EventHandlerBinding, eventmanager from app.runtime.extensions import module_manager as module_manager_extension from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.service_config import configure_service_config_reader from app.schemas.event import ConfigChangeEventData from app.schemas.types import EventType _SAMPLE_MANIFEST = """ schema_version = 1 id = "SampleModule" kind = "host_module" entrypoint = "fixture_sample_module:SampleModule" depends_on = [] [metadata] name = "Sample" type = "notification" subtype = "Telegram" priority = 10 [activation] policy = "when_configured" watch = ["Notifications"] [activation.selector] kind = "system_config_item" key = "Notifications" match_field = "type" match_value = "sample" enabled_field = "enabled" """ _OTHER_MANIFEST = """ schema_version = 1 id = "OtherModule" kind = "host_module" entrypoint = "fixture_other_module:OtherModule" depends_on = [] [metadata] name = "Other" type = "notification" subtype = "Telegram" priority = 20 [activation] policy = "when_configured" watch = ["Notifications"] [activation.selector] kind = "system_config_item" key = "Notifications" match_field = "type" match_value = "other" enabled_field = "enabled" """ _MODULE_SOURCE = """ class {class_name}: instances = [] def __init__(self): self.events = ["create"] type(self).instances.append(self) def init_module(self): self.events.append("start") def stop(self): self.events.append("stop") def test(self): return True, "ok" def capability_method(self): return "handled" @staticmethod def get_name(): return "{name}" @staticmethod def get_type(): return "notification" @staticmethod def get_subtype(): return "Telegram" @staticmethod def get_priority(): return {priority} """ def _write_capability(root: Path, directory: str, manifest: str) -> None: """写入一个合成 Host Module 声明。""" capability_dir = root / directory capability_dir.mkdir(parents=True) (capability_dir / "capability.toml").write_text( manifest.strip() + "\n", encoding="utf-8", ) def _build_registry(root: Path) -> CapabilityRegistry: """用生产 schema 构造只包含两个合成模块的 Registry。""" _write_capability(root, "sample", _SAMPLE_MANIFEST) _write_capability(root, "other", _OTHER_MANIFEST) return CapabilityRegistry.discover( roots=[root], kinds={"host_module"}, selector_schemas={ "system_config_item": SelectorSchema( required_fields=frozenset({ "key", "match_field", "match_value", "enabled_field", }), ), "setting_truthy": SelectorSchema( required_fields=frozenset({"key"}), ), }, ) def _config_changed_listeners() -> dict: """读取 ConfigChanged 监听快照,用于验证全局测试状态完整恢复。""" subscribers = getattr(eventmanager, "_EventManager__broadcast_subscribers") return dict(subscribers.get(EventType.ConfigChanged, {})) def _run_real_host_module_check(tmp_path: Path, body: str) -> None: """在隔离后端和进程内网络守卫下执行真实 Host Module 合同检查。""" project_root = Path(__file__).parents[1] prelude = r""" import ipaddress import socket import sys network_attempts = [] allowed_hosts = {"127.0.0.1", "::1", "localhost", "0.0.0.0", "::", ""} real_getaddrinfo = socket.getaddrinfo real_connect = socket.socket.connect def is_allowed_host(host): normalized = host.decode() if isinstance(host, (bytes, bytearray)) else host if normalized is None or normalized in allowed_hosts: return True try: address = ipaddress.ip_address(str(normalized).split("%", 1)[0]) return address.is_loopback or address.is_unspecified except ValueError: return False def block_network(operation, host): network_attempts.append((operation, host)) raise AssertionError(f"Host Module 合同测试禁止真实出站:{operation} {host!r}") def guarded_getaddrinfo(host, *args, **kwargs): if not is_allowed_host(host): block_network("DNS", host) return real_getaddrinfo(host, *args, **kwargs) def guarded_connect(sock, address): if isinstance(address, tuple) and address and not is_allowed_host(address[0]): block_network("socket", address[0]) return real_connect(sock, address) socket.getaddrinfo = guarded_getaddrinfo socket.socket.connect = guarded_connect from app.testing.bootstrap import prepare_backend prepare_backend() """ code = f"{prelude}\n{body}\nassert network_attempts == [], network_attempts\n" env = os.environ.copy() env["CONFIG_DIR"] = str(tmp_path / "config") env["PYTHONPATH"] = str(project_root) result = subprocess.run( [sys.executable, "-c", code], cwd=project_root, env=env, capture_output=True, text=True, timeout=180, check=False, ) assert result.returncode == 0, ( f"真实 Host Module 合同检查失败:\nstdout:\n{result.stdout[-4000:]}\n" f"stderr:\n{result.stderr[-8000:]}" ) @pytest.fixture def module_manager_harness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> Iterator[SimpleNamespace]: """用合成声明和内存配置隔离 ModuleManager 单例。""" source_root = tmp_path / "source" source_root.mkdir() (source_root / "fixture_sample_module.py").write_text( _MODULE_SOURCE.format(class_name="SampleModule", name="Sample", priority=10), encoding="utf-8", ) (source_root / "fixture_other_module.py").write_text( _MODULE_SOURCE.format(class_name="OtherModule", name="Other", priority=20), encoding="utf-8", ) monkeypatch.syspath_prepend(str(source_root)) registry = _build_registry(tmp_path / "capabilities") monkeypatch.setattr( module_manager_extension, "build_host_module_registry", lambda: registry, ) config_values = {"Notifications": []} def get_config(_self, key=None): key_value = getattr(key, "value", key) if key_value is None: return dict(config_values) return config_values.get(key_value) monkeypatch.setattr(SystemConfigOper, "get", get_config) previous_config_reader = configure_service_config_reader( lambda key: SystemConfigOper().get(key) ) singleton_key = (ModuleManager, (), frozenset()) previous_manager = Singleton._instances.pop(singleton_key, None) resolver_attr = "_EventManager__handler_instance_resolvers" previous_resolvers = dict(getattr(eventmanager, resolver_attr)) previous_config_changed_listeners = _config_changed_listeners() for module_name in ("fixture_sample_module", "fixture_other_module"): sys.modules.pop(module_name, None) manager = ModuleManager() restored = False def restore() -> None: """撤销 Manager 构造写入的单例、resolver 和事件监听器。""" nonlocal restored if restored: return try: manager.shutdown() except (AttributeError, CapabilityRuntimeClosedError): pass Singleton._instances.pop(singleton_key, None) if previous_manager is not None: Singleton._instances[singleton_key] = previous_manager setattr(eventmanager, resolver_attr, previous_resolvers) subscribers = getattr(eventmanager, "_EventManager__broadcast_subscribers") if previous_config_changed_listeners: subscribers[EventType.ConfigChanged] = dict( previous_config_changed_listeners ) else: subscribers.pop(EventType.ConfigChanged, None) for module_name in ("fixture_sample_module", "fixture_other_module"): sys.modules.pop(module_name, None) configure_service_config_reader(previous_config_reader) restored = True try: yield SimpleNamespace( manager=manager, config_values=config_values, previous_config_changed_listeners=previous_config_changed_listeners, restore=restore, ) finally: restore() def _enable_sample(config_values: dict) -> None: """写入可通过 sample selector 的最小合法通知配置。""" config_values["Notifications"] = [ { "name": "sample", "type": "sample", "config": {}, "switchs": [], "enabled": True, } ] def test_harness_restores_module_manager_config_listener( module_manager_harness, ) -> None: """Fixture teardown 不能把临时 Manager 的 bound listener 留在全局事件总线。""" manager = module_manager_harness.manager current_listeners = _config_changed_listeners() assert current_listeners != ( module_manager_harness.previous_config_changed_listeners ) assert any( getattr(listener, "__self__", None) is manager for listener in current_listeners.values() ) module_manager_harness.restore() assert _config_changed_listeners() == ( module_manager_harness.previous_config_changed_listeners ) def test_specs_are_lightweight_and_do_not_materialize_modules( module_manager_harness, ) -> None: """ModuleManager 的声明视图不能解析任何 Host Module 实现。""" manager = module_manager_harness.manager specs = manager.list_specs() assert manager.get_specs() == specs assert [spec.id for spec in specs] == ["OtherModule", "SampleModule"] assert [spec.metadata["name"] for spec in specs] == ["Other", "Sample"] assert "fixture_sample_module" not in sys.modules assert "fixture_other_module" not in sys.modules assert manager.get_running_module("SampleModule") is None def test_get_module_materializes_one_canonical_class_without_starting_it( module_manager_harness, ) -> None: """兼容查询返回 canonical class,但不创建或启动资源。""" manager = module_manager_harness.manager module_class = manager.get_module("SampleModule") canonical_class = importlib.import_module( "fixture_sample_module" ).SampleModule assert module_class is canonical_class assert manager.get_module("SampleModule") is canonical_class assert canonical_class.instances == [] assert manager.get_running_module("SampleModule") is None assert "fixture_other_module" not in sys.modules def test_get_modules_materializes_all_real_classes_without_starting_them( module_manager_harness, ) -> None: """旧 get_modules 合同保留真实 class 字典,不返回代理或隐式激活。""" manager = module_manager_harness.manager modules = manager.get_modules() sample_module = importlib.import_module("fixture_sample_module") other_module = importlib.import_module("fixture_other_module") assert modules == { "OtherModule": other_module.OtherModule, "SampleModule": sample_module.SampleModule, } assert sample_module.SampleModule.instances == [] assert other_module.OtherModule.instances == [] assert manager.get_running_module("SampleModule") is None assert manager.get_running_module("OtherModule") is None def test_config_reconcile_reload_and_stop_preserve_manager_contract( module_manager_harness, monkeypatch: pytest.MonkeyPatch, ) -> None: """配置激活、全量 reload 与可重启 stop 保持同步可观察顺序。""" manager = module_manager_harness.manager _enable_sample(module_manager_harness.config_values) manager.load_modules() first = manager.get_running_module("SampleModule") assert first is not None assert first.events == ["create", "start"] send_event = Mock() monkeypatch.setattr(eventmanager, "send_event", send_event) manager.reload() second = manager.get_running_module("SampleModule") assert second is not None assert second is not first assert first.events == ["create", "start", "stop"] assert second.events == ["create", "start"] send_event.assert_called_once_with(etype=EventType.ModuleReload, data={}) manager.stop() assert manager.get_running_module("SampleModule") is None assert second.events == ["create", "start", "stop"] manager.load_modules() restarted = manager.get_running_module("SampleModule") assert restarted is not None assert restarted is not second assert restarted.events == ["create", "start"] module_manager_harness.config_values["Notifications"] = [] manager.load_modules() assert manager.get_running_module("SampleModule") is None assert restarted.events == ["create", "start", "stop"] def test_config_event_reloads_same_instance_and_tracks_selector_changes( module_manager_harness, ) -> None: """配置事件由 Host Adapter 唯一协调,并保留模块实例内的重载状态。""" manager = module_manager_harness.manager _enable_sample(module_manager_harness.config_values) manager.load_modules() running = manager.get_running_module("SampleModule") manager.handle_config_changed( Event( EventType.ConfigChanged, ConfigChangeEventData(key="Notifications"), ) ) assert manager.get_running_module("SampleModule") is running assert running.events == ["create", "start", "stop", "start"] module_manager_harness.config_values["Notifications"] = [] manager.handle_config_changed( Event( EventType.ConfigChanged, ConfigChangeEventData(key="Notifications"), ) ) assert manager.get_running_module("SampleModule") is None assert running.events == ["create", "start", "stop", "start", "stop"] def test_shutdown_is_irreversible(module_manager_harness) -> None: """shutdown 撤销全部可见实例,并拒绝通过 load_modules 再次启动。""" manager = module_manager_harness.manager _enable_sample(module_manager_harness.config_values) manager.load_modules() running = manager.get_running_module("SampleModule") assert running is not None manager.shutdown() assert manager.get_running_module("SampleModule") is None assert running.events == ["create", "start", "stop"] manager.load_modules() assert manager.get_running_module("SampleModule") is None assert type(running).instances == [running] def test_all_real_host_modules_zero_arg_construct_without_starting_resources( tmp_path: Path, ) -> None: """每份真实 manifest 都必须能解析 canonical class 并零参数构造且不启动资源。""" body = r""" from app.runtime.extensions.host_module_adapter import ( HostModuleAdapter, build_host_module_registry, ) registry = build_host_module_registry() specs = registry.list_specs() assert len(specs) == 39 adapter = HostModuleAdapter() lifecycle_events = [] instances = {} def make_recorder(operation, capability_id): def record(instance): lifecycle_events.append((operation, capability_id, id(instance))) return record for spec in specs: implementation = adapter.materialize(spec) module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1) assert implementation is getattr(sys.modules[module_name], symbol_name) implementation.init_module = make_recorder("start", spec.id) implementation.stop = make_recorder("stop", spec.id) instance = adapter.create(spec, implementation, generation=1) assert type(instance) is implementation instances[spec.id] = instance assert set(instances) == {spec.id for spec in specs} assert lifecycle_events == [] """ _run_real_host_module_check(tmp_path, body) def test_real_manifest_inventory_drives_full_module_manager_lifecycle( tmp_path: Path, ) -> None: """真实声明自动驱动全量激活、原实例重载、禁用停止和不可逆关闭门禁。""" body = r""" from app.db.oper.systemconfig import SystemConfigOper from app.runtime.capabilities.model import ActivationPolicy from app.runtime.config import settings from app.runtime.events import Event from app.runtime.extensions.host_module_adapter import ( HostModuleAdapter, build_host_module_registry, ) from app.runtime.extensions.service_config import configure_service_config_reader from app.schemas.event import ConfigChangeEventData from app.schemas.types import EventType registry = build_host_module_registry() specs = registry.list_specs() assert len(specs) == 39 spec_by_id = {spec.id: spec for spec in specs} events = {spec.id: [] for spec in specs} adapter = HostModuleAdapter() def make_recorder(operation, capability_id): def record(instance): events[capability_id].append((operation, id(instance))) return record for spec in specs: implementation = adapter.materialize(spec) implementation.init_module = make_recorder("start", spec.id) implementation.stop = make_recorder("stop", spec.id) config_values = {} enabled_service_values = {} selector_keys = set() configured_ids = set() for spec in specs: if spec.activation is not ActivationPolicy.WHEN_CONFIGURED: continue configured_ids.add(spec.id) selector = spec.selector assert selector is not None key = str(selector.config["key"]) selector_keys.add(key) if selector.kind == "setting_truthy": setattr(settings, key, f"enabled:{spec.id}") elif selector.kind == "system_config_item": enabled_service_values.setdefault(key, []).append({ "name": f"contract-{spec.id}", "type": selector.config["match_value"], "config": {}, "enabled": True, }) else: raise AssertionError(f"未覆盖的 Host Module selector:{selector.kind}") config_values.update({key: list(value) for key, value in enabled_service_values.items()}) def get_config(_self, key=None): key_value = getattr(key, "value", key) if key_value is None: return dict(config_values) return config_values.get(key_value) SystemConfigOper.get = get_config configure_service_config_reader(lambda key: SystemConfigOper().get(key)) from app.runtime.extensions.module_manager import ModuleManager manager = ModuleManager() bootstrap_ids = { spec.id for spec in specs if spec.activation is ActivationPolicy.BOOTSTRAP } initial_ids = bootstrap_ids | configured_ids assert bootstrap_ids assert configured_ids assert initial_ids == {spec.id for spec in specs} initial_instances = { capability_id: manager.get_running_module(capability_id) for capability_id in initial_ids } assert all(initial_instances.values()) assert { capability_id: [operation for operation, _instance_id in events[capability_id]] for capability_id in initial_ids } == {capability_id: ["start"] for capability_id in initial_ids} watch_keys = {key for spec in specs for key in spec.watch} watched_ids = { spec.id for spec in specs if spec.id in initial_ids and watch_keys.intersection(spec.watch) } manager.handle_config_changed( Event( EventType.ConfigChanged, ConfigChangeEventData(key=watch_keys), ) ) for capability_id, initial_instance in initial_instances.items(): assert manager.get_running_module(capability_id) is initial_instance operations = [operation for operation, _instance_id in events[capability_id]] expected = ["start", "stop", "start"] if capability_id in watched_ids else ["start"] assert operations == expected, (capability_id, operations) assert { instance_id for _operation, instance_id in events[capability_id] } == {id(initial_instance)} for spec in specs: if spec.id not in configured_ids: continue selector = spec.selector key = str(selector.config["key"]) if selector.kind == "setting_truthy": setattr(settings, key, False) for key in enabled_service_values: config_values[key] = [] manager.handle_config_changed( Event( EventType.ConfigChanged, ConfigChangeEventData(key=selector_keys), ) ) for capability_id in configured_ids: assert manager.get_running_module(capability_id) is None assert [operation for operation, _instance_id in events[capability_id]] == [ "start", "stop", "start", "stop", ] for capability_id in bootstrap_ids: assert manager.get_running_module(capability_id) is initial_instances[capability_id] manager.shutdown() assert all(manager.get_running_module(spec.id) is None for spec in specs) events_after_shutdown = { capability_id: list(capability_events) for capability_id, capability_events in events.items() } for spec in specs: if spec.id not in configured_ids: continue selector = spec.selector key = str(selector.config["key"]) if selector.kind == "setting_truthy": setattr(settings, key, f"re-enabled:{spec.id}") for key, value in enabled_service_values.items(): config_values[key] = list(value) manager.load_modules() manager.handle_config_changed( Event( EventType.ConfigChanged, ConfigChangeEventData(key=watch_keys), ) ) assert all(manager.get_running_module(spec.id) is None for spec in specs) assert events == events_after_shutdown assert set(spec_by_id) == set(events) """ _run_real_host_module_check(tmp_path, body) def test_default_config_keeps_every_manifest_configured_entrypoint_unimported( tmp_path: Path, ) -> None: """默认配置惰性边界由全部 when-configured manifest 自动生成。""" body = r""" from app.db.oper.systemconfig import SystemConfigOper from app.runtime.capabilities.model import ActivationPolicy from app.runtime.config import settings from app.runtime.extensions.host_module_adapter import ( HostModuleAdapter, build_host_module_registry, ) registry = build_host_module_registry() specs = registry.list_specs() assert len(specs) == 39 configured_specs = tuple( spec for spec in specs if spec.activation is ActivationPolicy.WHEN_CONFIGURED ) configured_modules = { spec.entrypoint.split(":", maxsplit=1)[0] for spec in configured_specs } assert configured_modules assert configured_modules.isdisjoint(sys.modules) for spec in configured_specs: selector = spec.selector assert selector is not None if selector.kind == "setting_truthy": setattr(settings, str(selector.config["key"]), False) SystemConfigOper.get = lambda _self, key=None: {} if key is None else [] adapter = HostModuleAdapter() for spec in specs: if spec.activation is not ActivationPolicy.BOOTSTRAP: continue implementation = adapter.materialize(spec) implementation.init_module = lambda _self: None implementation.stop = lambda _self: None assert configured_modules.isdisjoint(sys.modules) from app.runtime.extensions.module_manager import ModuleManager manager = ModuleManager() assert manager.get_specs() == manager.list_specs() assert {spec.id for spec in manager.list_specs()} == {spec.id for spec in specs} assert all(manager.get_running_module(spec.id) is None for spec in configured_specs) assert configured_modules.isdisjoint(sys.modules) manager.shutdown() assert configured_modules.isdisjoint(sys.modules) """ _run_real_host_module_check(tmp_path, body) def test_event_resolver_uses_exact_class_and_blocks_stopped_owner_fallback( module_manager_harness, ) -> None: """同名 class 不能冒充 owner;已停止 owner 必须返回 Binding(None)。""" manager = module_manager_harness.manager _enable_sample(module_manager_harness.config_values) manager.load_modules() module_class = manager.get_module("SampleModule") running = manager.get_running_module("SampleModule") active_binding = manager.resolve_event_handler_instance(module_class) assert active_binding == EventHandlerBinding( instance=running, owner_name="Sample", ) impostor = type("SampleModule", (), {}) impostor.__module__ = module_class.__module__ assert manager.resolve_event_handler_instance(impostor) is None manager.stop() stopped_binding = manager.resolve_event_handler_instance(module_class) assert stopped_binding == EventHandlerBinding( instance=None, owner_name="Sample", ) def test_default_modulelist_does_not_import_unconfigured_provider_sdks( tmp_path: Path, ) -> None: """默认配置下构造 Manager 和查询模块列表都不能拉起重量 provider SDK。""" project_root = Path(__file__).parents[1] code = """ from app.testing.bootstrap import prepare_backend prepare_backend() from app.db.oper.systemconfig import SystemConfigOper from app.runtime.config import settings def empty_config(self, key=None): return {} if key is None else [] SystemConfigOper.get = empty_config settings.ACOUSTID_API_KEY = None settings.FANART_API_KEY = None from app.runtime.extensions.module_manager import ModuleManager from app.application.module import configure_module_runtime configure_module_runtime(lambda: ModuleManager()) manager = ModuleManager() assert len(manager.list_specs()) == 39 assert manager.get_specs() == manager.list_specs() from app.api.endpoints.system import modulelist response = modulelist(None) assert len(response.data["modules"]) == 39 heavy_prefixes = ( "lark_oapi", "slack_bolt", "slack_sdk", "discord", "plexapi", "telebot", ) loaded = sorted( name for name in sys.modules if any(name == prefix or name.startswith(prefix + ".") for prefix in heavy_prefixes) ) assert loaded == [], loaded manager.shutdown() """ env = os.environ.copy() env["CONFIG_DIR"] = str(tmp_path / "config") env["PYTHONPATH"] = str(project_root) result = subprocess.run( [sys.executable, "-c", "import sys\n" + code], cwd=project_root, env=env, capture_output=True, text=True, timeout=180, check=False, ) assert result.returncode == 0, ( f"子进程模块发现失败:\nstdout:\n{result.stdout[-2000:]}\n" f"stderr:\n{result.stderr[-4000:]}" ) def test_lazy_boundary_annotations_are_reflectable_without_provider_sdks( tmp_path: Path, ) -> None: """宿主公共注解可被反射,且反射过程不加载可选 provider SDK。""" project_root = Path(__file__).parents[1] code = """ from app.testing.bootstrap import prepare_backend prepare_backend() import sys from typing import Any, Optional, get_type_hints provider_prefixes = ("qbittorrentapi", "transmission_rpc", "pywebpush") def loaded_provider_modules(): return sorted( name for name in sys.modules if any( name == prefix or name.startswith(prefix + ".") for prefix in provider_prefixes ) ) assert loaded_provider_modules() == [] from app.chain import ChainBase from app.api.endpoints.message import WebPushError, is_webpush_subscription_gone assert get_type_hints(ChainBase.torrent_files)["return"] == Optional[Any] assert get_type_hints(is_webpush_subscription_gone)["error"] is WebPushError assert loaded_provider_modules() == [] """ env = os.environ.copy() env["CONFIG_DIR"] = str(tmp_path / "config") env["PYTHONPATH"] = str(project_root) result = subprocess.run( [sys.executable, "-c", code], cwd=project_root, env=env, capture_output=True, text=True, timeout=180, check=False, ) assert result.returncode == 0, ( f"轻量注解反射失败:\nstdout:\n{result.stdout[-2000:]}\n" f"stderr:\n{result.stderr[-4000:]}" ) def test_manifest_metadata_matches_legacy_module_class_contract(tmp_path: Path) -> None: """manifest 投影必须与插件仍可调用的模块类 metadata 完全一致。""" project_root = Path(__file__).parents[1] code = """ from app.testing.bootstrap import prepare_backend prepare_backend() from app.db.oper.systemconfig import SystemConfigOper SystemConfigOper.get = lambda self, key=None: {} if key is None else [] from app.runtime.config import settings settings.ACOUSTID_API_KEY = None settings.FANART_API_KEY = None from app.runtime.extensions.module_manager import ModuleManager manager = ModuleManager() modules = manager.get_modules() assert len(modules) == len(manager.list_specs()) == 39 for spec in manager.list_specs(): implementation = modules[spec.id] assert implementation.get_name() == spec.metadata["name"] assert implementation.get_type().value == spec.metadata["type"] assert implementation.get_subtype().name == spec.metadata["subtype"] assert implementation.get_priority() == spec.metadata["priority"] manager.shutdown() """ env = os.environ.copy() env["CONFIG_DIR"] = str(tmp_path / "config") env["PYTHONPATH"] = str(project_root) result = subprocess.run( [sys.executable, "-c", "import sys\n" + code], cwd=project_root, env=env, capture_output=True, text=True, timeout=180, check=False, ) assert result.returncode == 0, ( f"模块 metadata 兼容检查失败:\nstdout:\n{result.stdout[-2000:]}\n" f"stderr:\n{result.stderr[-4000:]}" )