Skip to content

fix(core): bundle every RPC domain into frozen build + regression coverage#353

Merged
clstaudt merged 1 commit into
mainfrom
fix/core-bundle-all-domains
Jun 28, 2026
Merged

fix(core): bundle every RPC domain into frozen build + regression coverage#353
clstaudt merged 1 commit into
mainfrom
fix/core-bundle-all-domains

Conversation

@clstaudt

Copy link
Copy Markdown
Contributor

Summary

The distributed macOS app failed when committing from AI Document Import with:

Error invoking remote method 'rpc': Error: No module named 'tuttle.app.imports'

Root cause: the frozen PyInstaller core resolves intent modules dynamically via importlib (tuttle.app.{domain}.intent), so PyInstaller's static analyzer can't trace them. Each domain therefore had to be hand-listed in tuttle-rpc.spec's hiddenimports. The imports domain (and, latently, invoice_notes) was never added, so it was silently dropped from the release binary — even though it worked fine in dev. A missing __init__.py (fixed in #351) was necessary but not sufficient.

Changes

  • Real fix: replace the fragile hand-maintained hiddenimports list with collect_submodules("tuttle.app"), so every present and future domain is always bundled. Verified it discovers all 19 intent modules, including imports and invoice_notes.
  • Unit guards (tuttle_tests/test_rpc_dispatch.py):
    • every tuttle/app/<domain> is a real importable package with exactly one *Intent class (catches missing __init__.py)
    • collect_submodules covers every on-disk domain (catches spec drift); skips if PyInstaller absent
  • End-to-end smoke test (scripts/smoke_core.py): spawns the actual frozen binary and probes every domain, asserting none returns "No module named". Wired into CI before the expensive packaging step (fail-fast), into pack_electron.py, and as a just smoke-core recipe.
  • Terminology: renamed the Python core from "sidecar" to "core" across build scripts, spec, workflow, bridge, and docs.

Why the previous coverage missed this

The existing test_rpc_dispatch.py runs against the source tree, so it structurally cannot catch a packaging regression in the frozen binary. The new guards target the exact mechanism the release build depends on.

Test plan

  • uv run pytest -q — 336 passed
  • Negative check: removing tuttle/app/imports/__init__.py makes both unit guards fail with a precise diagnostic
  • Built the frozen binary and ran scripts/smoke_core.py — all 19 domains bundled & importable
  • CI green on the new "Smoke-test core" step across all platforms

Made with Cursor

…erage

The frozen PyInstaller core resolved intent modules dynamically via
importlib, so each domain had to be hand-listed in tuttle-rpc.spec's
hiddenimports. The 'imports' (and latently 'invoice_notes') domain was
missing, so the distributed app failed with "No module named
'tuttle.app.imports'" on document-import commit, despite working in dev.

- Replace the hand-maintained hiddenimports list with
  collect_submodules("tuttle.app") so any present-or-future domain is
  always bundled.
- Add unit guards (test_rpc_dispatch.py): every tuttle/app/<domain> is an
  importable package, and collect_submodules covers every domain.
- Add scripts/smoke_core.py: spawns the actual frozen binary and probes
  every domain; wired into CI (pre-packaging, fail-fast), pack_electron,
  and a `just smoke-core` recipe.
- Rename build terminology "sidecar" -> "core" throughout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@clstaudt clstaudt added this pull request to the merge queue Jun 28, 2026
Merged via the queue into main with commit ba2ecab Jun 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant