diff --git a/backend/tests/test_packaging_spec.py b/backend/tests/test_packaging_spec.py new file mode 100644 index 00000000..2f867135 --- /dev/null +++ b/backend/tests/test_packaging_spec.py @@ -0,0 +1,31 @@ +"""tsp.spec 打包不变量 — 运行时动态发现的模块必须随冻结产物发布 (issue #459)。 + +app/extensions/loader.py 用 importlib.import_module + pkgutil.iter_modules 在 +运行时发现 app/custom/*, 两条链路对 PyInstaller 静态分析都不可见; tsp.spec +未声明时冻结产物里扩展静默消失(桌面版 AI 对话助手 405, 且无任何日志)。 +锁定双声明契约: + - hiddenimports(collect_submodules) 进 PYZ —— 保 import_module 可导入; + - datas 落盘到 app/custom —— frozen 下 app.__path__ 解析到 _internal/app, + 保 pkgutil.iter_modules 可枚举(与 fuyao 插件同一处理方式)。 +""" + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +SPEC_TEXT = (REPO_ROOT / "packaging" / "tsp.spec").read_text(encoding="utf-8") +# 只断言代码行, 避免被注释里的字样误判为已声明 +SPEC_CODE = "\n".join( + line for line in SPEC_TEXT.splitlines() if not line.lstrip().startswith("#") +) + + +def test_spec_declares_custom_extensions(): + """app/custom/* 双声明: PYZ 可导入 + 磁盘可枚举, 缺一冻结后扩展静默消失。""" + assert 'collect_submodules("app.custom")' in SPEC_CODE + assert '"app/custom"' in SPEC_CODE + + +def test_spec_declares_builtin_plugin_fuyao(): + """fuyao 插件双声明不被回归 (同类动态发现问题, 见 spec 内注释)。""" + assert "app.plugins.fuyao.provider" in SPEC_CODE + assert "app/plugins/fuyao" in SPEC_CODE diff --git a/packaging/tsp.spec b/packaging/tsp.spec index a2b5e317..072776ad 100644 --- a/packaging/tsp.spec +++ b/packaging/tsp.spec @@ -76,6 +76,17 @@ hiddenimports += [ "app.plugins.fuyao.client", ] +# ── 后端源码扩展 (app/custom/*) ────────────────────────────────────── +# extensions/loader.py 运行时用 importlib.import_module + pkgutil.iter_modules +# 发现 app/custom/* — 与插件同样对静态分析不可见, 必须双声明 (issue #459: +# 未声明时冻结包里扩展静默消失, 助手路由未注册 → 请求落到 GET-only SPA 兜底 +# 返回 405): +# datas 落盘到 _internal/app/custom — frozen 下 app.__path__ 解析到 +# _internal/app, 保 pkgutil 枚举与磁盘可见性; +# hiddenimports 进 PYZ — 保 importlib.import_module 可导入。 +datas += [(str(ROOT / "backend" / "app" / "custom"), "app/custom")] +hiddenimports += collect_submodules("app.custom") + # ── pywebview 平台后端 (动态导入, PyInstaller 默认抓不到) ──────────── hiddenimports += collect_submodules("webview") hiddenimports += collect_submodules("webview.platforms")