""" inline 插件 [workers.config] 对齐(网关侧兜底)单测 —— WorkerManager.align_inline_config。 网关侧在创建 inline 副本之前,按「配置优先来源模式」(根 config.toml 的 [cache].launch_worker_count)对齐插件配置:cache 模式取共享缓存里的段, toml 模式以 config/workers//config.toml 为准并覆盖缓存。目的是让网关与 worker 包版本解耦——worker 包仍是旧版时,旧版 on_init 只拿到 toml 展平配置, 会出现“前端显示缓存值、实际跑 toml”。 用 dict 作缓存桩,验证两种模式下插件拿到的配置与缓存/文件两侧的取值一致。 """ import os import sys REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(REPO, "src")) os.chdir(REPO) # 网关模块导入期读根 config.toml / 日志配置 from chongming.app.core.worker_manager import _CONFIGS_KEY, WorkerManager # noqa: E402 PASS = 0 FAIL = 0 WT = "aibox_sophon_adapter" # _discover_plugins 展平出来的形态(toml 值) FLAT = {"type": WT, "count": 1, "data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554"} # 该插件 config.toml 的 [workers.config] 原始段 SECTION = {"data_dir": "./data", "mediamtx_addr": "rtsp://toml:8554", "heartbeat": {"timeout": 15}} # 前端下发后缓存里的段(改过 mediamtx_addr 与 heartbeat) CACHED = {"data_dir": "./data", "mediamtx_addr": "rtsp://cache:9999", "heartbeat": {"timeout": 30}} def report(name, cond, detail=""): global PASS, FAIL if cond: PASS += 1 print(f"✅ {name}" + (f" — {detail}" if detail else "")) else: FAIL += 1 print(f"❌ {name}" + (f" — {detail}" if detail else "")) def manager(cache, mode): m = WorkerManager.__new__(WorkerManager) # 免会话构造:仅用到 cache + config_mode m.cache = cache m._launch_worker_count = mode return m def run() -> int: key = _CONFIGS_KEY.format(type=WT) # ── 1. cache 模式:缓存有段 → 插件拿到缓存值(本次修复目标)────────── cache = {key: dict(CACHED)} got = manager(cache, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION)) report( "cache 模式:插件配置取缓存值(覆盖 toml 同名键)", got.get("mediamtx_addr") == "rtsp://cache:9999" and got.get("heartbeat") == {"timeout": 30}, f"mediamtx_addr={got.get('mediamtx_addr')}", ) report("cache 模式:不覆盖已存在的缓存段", cache[key] == CACHED, f"cache={cache[key]}") # ── 2. cache 模式:缓存缺失 → 用 toml 段引导并写回 ────────────────── cache2 = {} got2 = manager(cache2, "cache").align_inline_config(WT, dict(FLAT), dict(SECTION)) report( "cache 模式:缓存缺失用 config.toml 引导并写回", cache2.get(key) == SECTION and got2.get("mediamtx_addr") == "rtsp://toml:8554", f"cache={cache2.get(key)}", ) # ── 3. toml 模式:以文件为准并覆盖缓存(前端随之显示文件值)───────── cache3 = {key: dict(CACHED)} got3 = manager(cache3, "toml").align_inline_config(WT, dict(FLAT), dict(SECTION)) report( "toml 模式:插件配置为 toml 值", got3.get("mediamtx_addr") == "rtsp://toml:8554", f"mediamtx_addr={got3.get('mediamtx_addr')}", ) report( "toml 模式:启动时用 toml 覆盖缓存(前端与运行值一致)", cache3.get(key) == SECTION, f"cache={cache3.get(key)}", ) # ── 4. 边界:无缓存后端 / 无段(旧 config/workers/config.toml 兼容路径)── cache4 = {key: dict(CACHED)} m4 = manager(cache4, "cache") m4.cache = None report( "无缓存后端:原样返回(经典 toml 行为)", m4.align_inline_config(WT, dict(FLAT), dict(SECTION)) == FLAT and cache4[key] == CACHED, ) cache5 = {} got5 = manager(cache5, "toml").align_inline_config(WT, dict(FLAT), None) report("无 [workers.config] 段(旧写法):不写缓存、原样返回", got5 == FLAT and not cache5) # ── 5. 与 SDK 侧一致:缓存键必须相同;SDK 为本地版本时才比对解析结果 ── # 注意:venv 里的 chongming-worker 可能是索引里的发布版(uv sync 会把可编辑安装 # 换回锁文件版本),新版才有 resolve_startup_config / 正确的 toml 分支语义。 sys.path.insert(0, os.path.join(REPO, "utils", "worker", "src")) import chongming_worker.runtime as r # noqa: E402 report( "缓存键与 SDK 一致(两端必须读写同一键)", r._configs_cache_key(WT) == _CONFIGS_KEY.format(type=WT), f"sdk={r._configs_cache_key(WT)}", ) local_src = os.path.join(REPO, "utils", "worker", "src") in os.path.abspath(r.__file__) if not local_src: print(f"⚠️ 跳过解析结果比对:当前 SDK 为 {r.__file__}(非本地源码,缺本次改动)") else: c = {r._configs_cache_key(WT): dict(CACHED)} sdk = r.resolve_runtime_config(WT, dict(FLAT), c, "cache") report( "cache 模式:网关侧兜底与 SDK 解析结果一致", sdk.get("mediamtx_addr") == CACHED["mediamtx_addr"], f"sdk={sdk.get('mediamtx_addr')}", ) # ── 6. 端到端:网关对齐 + 插件初始化(部署组合:网关新、worker 包为发布版)── # 该断言即线上现象的复现与验证:插件 on_init 拿到的必须是网关对齐后的缓存值, # 与 venv 里 chongming-worker 是否为含本次改动的版本无关。 import asyncio from chongming_worker.plugin import Plugin # noqa: E402 seen = {} class DemoPlugin(Plugin): type = WT auto_bind = False async def _bootstrap(self): # 隔离数据库/对象存储引导,只看配置来源 return None async def on_init(self): seen["on_init"] = self.config.get("mediamtx_addr") def _create_workers_impl(self, session, config, count=1, worker_manager=None): seen["impl"] = (config or {}).get("mediamtx_addr") return [] aligned = manager({key: dict(CACHED)}, "cache").align_inline_config( WT, dict(FLAT), dict(SECTION) ) asyncio.run(DemoPlugin().create_workers(None, aligned, 1)) report( "端到端:网关对齐后插件 on_init 读到缓存值(不依赖 worker 包版本)", seen.get("on_init") == CACHED["mediamtx_addr"] == seen.get("impl"), f"on_init={seen.get('on_init')}, impl={seen.get('impl')}, " f"sdk={os.path.abspath(__import__('chongming_worker').__file__)}", ) print("\n" + "=" * 56) print(f"测试结果: ✅ {PASS} 通过 ❌ {FAIL} 失败") return 0 if FAIL == 0 else 1 if __name__ == "__main__": sys.exit(run())