跳到正文
DEV Community· Krish Verma·· 4 小時前AI 評分42

Ankita 2.7.0 缺少模組導致桌面應用程式無法啟動:如何測試已封裝的匯入依賴圖

How I built a test that walks the shipped import graph: the missing module that bricked Ankita 2.7.0

AI 導讀

Ankita 作者為防止 2.7.0 因缺少封裝模組而無法啟動,為桌面應用程式加入測試,檢查已封裝程式碼的相對匯入是否都包含在 ASAR 檔案集中。測試沿用 electron-builder 的檔案匹配器,找出缺少的 sign-in.mjs;v2.7.1 將 companion 目錄加入封裝清單,並加入匯入依賴圖測試。

正文

How I built a test that walks the shipped import graph: the missing module that bricked Ankita 2.7.0

Ankita is my open-source desktop AI companion — local-first, MIT-licensed, your models. Yesterday I shipped v2.7.0. This morning I shipped v2.7.1, and the only reason it exists is embarrassing: after updating to 2.7.0, the desktop app would not start at all.

The main process died with ERR_MODULE_NOT_FOUND for desktop/chatgpt-companion/sign-in.mjs. The fix itself was one line in a YAML file. The interesting part is the test I wrote so it can never happen again — a test that walks the shipped import graph and fails the suite when a packaged module imports something that is not packaged.

Two copies of one tree, and a gap between them

Ankita's ChatGPT companion has a split personality, and that is where the bug lived.

The packaging config listed desktop/electron/**/* and desktop/browser-helper/**/*, but not desktop/chatgpt-companion/**/*. So the ASAR shipped the importer, not the imported module — and inside the ASAR, a relative import resolves inside the ASAR, so the extraResources copy sitting on disk in the same install could not save it.

The companion tree (desktop/chatgpt-companion/) ships twice. Once as real files under extraResources, because a Chrome extension cannot load code out of an ASAR archive. And once inside the ASAR, because the Electron main process imports companion modules directly — desktop/electron/chatgpt-sign-in.mjs imports signInCookies and friends straight from ../chatgpt-companion/sign-in.mjs.

Why did nobody catch it before release? In development, every file exists on disk. The import resolves, the tests pass, the app runs. The failure mode only exists in the packaged artifact, and nothing in the suite ever looked at the packaged artifact's file set. The unit tests tested the code. Nobody tested the box the code ships in.

The test: reuse the builder's own matcher

The new test lives in test/desktop/chatgpt-package.test.mjs, named every local module imported by packaged sources ships inside the ASAR file set. Its core idea is simple: do not reimplement electron-builder's file matching. Use electron-builder's file matching.

It loads desktop/packaging/electron-builder.yml, hands its files list to FileMatcher from app-builder-lib — the builder's own matcher class — and builds the exact filter the packager would use. That choice is the whole design. A hand-rolled glob check would drift from the builder's semantics the first time someone writes a tricky negation pattern, and then the test would be green while the package is broken.

From there it is mechanical:

  1. Enumerate every module under the trees that can contribute shipped code — desktop, services, src, tools, skills, vendor — with a .mjs, .js, .cjs, .mts, or .json extension.
  2. Keep only the files the filter says are packaged.
  3. Scan each packaged file's text for relative imports: static import/export ... from statements and literal dynamic import() calls, via two regexes.
  4. Resolve each relative specifier against the importing file's directory. A target that exists on disk but does not ship goes into a missing map, keyed by target with the list of importers.
  5. Assert the map is empty: "Every relative module imported by packaged code must be included in the ASAR file set."

Against the v2.7.0 config, the test fails with exactly one entry: desktop/chatgpt-companion/sign-in.mjs, imported by desktop/electron/chatgpt-sign-in.mjs. That is the production crash, reproduced as a unit-test failure. The v2.7.1 fix adds desktop/chatgpt-companion/**/* to the builder's files list — with a comment explaining why both copies exist — and the test goes green.

What the test deliberately does not do

A few boundaries are worth stating, because they are the places this test can still be fooled:

  • It only sees literal specifiers. A dynamic import built from a template string or a variable is invisible to the regexes. If a packaged module ever loads code that way, this test will not catch a missing target.
  • It only checks relative imports. Bare package imports are the bundler's problem, not the file list's.
  • It checks the file list, not the built artifact. It never builds an ASAR or boots the main process. It trusts that the builder's matcher, driven by the same config, predicts the real package contents.

That last one is the honest trade-off. A test that builds the real package and boots it headless would catch more — including a module that is listed but corrupted, or a native dependency the matcher cannot see. But it would also take minutes, need a headless Electron harness, and flake in CI for unrelated reasons. The static walk runs in milliseconds and fails on exactly the class of bug that just shipped.

The changelog, behind the mechanism

For completeness: v2.7.1 is only this fix. The companion tree is now packaged in the ASAR as well as its existing real-file extraResources copy, and the import-graph test guards the file list from here on. Release notes and installers are at https://github.com/akyourowngames/A.N.K.I.T.A/releases/tag/v2.7.1, and the project itself at https://github.com/akyourowngames/A.N.K.I.T.A.

Tell me I'm wrong

Here is the question I actually want argued with: is a regex walk over the file list enough, or a false sense of security? It trusts the builder's matcher to predict the artifact, and it is blind to computed imports. Would you ship the static check and move on, or is the only honest version a test that builds the real ASAR and boots the main process from it — cost and flakes included? Where exactly would you expect the static walk to break first?

來源:DEV Community · dev.to