Audit metadata
- Priority: P1
- Estimated effort: M
- Implementation risk: MED
- Category: tests
- Evidence baseline:
07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)
Dependencies
- P1 proposal 001 — Gate every release on the exact commit that passed CI. Included in the current P1 issue wave.
- P1 proposal 017 — Make the CI and release toolchain immutable and reproducible. Included in the current P1 issue wave.
Description and impact
The existing desktop post-pack check proves that one embedded server file was
copied, but it does not launch the packaged app. Packaging can still ship an app
whose embedded Next server fails, whose native PTY module cannot load, or whose
health endpoint never becomes reachable. A bounded end-to-end smoke on the
unpacked production bundle catches those failures before release assets are
created.
Current state
frontend/package.json:29-38 defines packaged desktop:pack and release
desktop:dist builds.
frontend/desktop/electron-builder.yml:7-58 selects app files, native PTY
modules, embedded standalone resources, targets, and afterPack.
frontend/scripts/electron-builder-after-pack.mjs:30-50 asserts only that a
nested or flat server.js exists.
frontend/desktop/logic/app-server.ts:38-64 persists the embedded frontend
port in embedded-frontend.port; startup waits for the server before showing
the app.
frontend/desktop/preload.ts:18-24 exposes localStudioDesktop.terminal.status;
that IPC call loads @lydell/node-pty and reports availability.
- Desktop startup supports an isolated user-data directory and updater-disable
environment setting; use both so CI cannot touch developer state or update.
- Current CI never launches an electron-builder output.
Verification commands
| Purpose |
Command |
Expected on success |
| Build unpacked app |
npm --prefix frontend run desktop:pack |
platform unpacked app exists |
| Smoke |
npm --prefix frontend run desktop:smoke |
health and PTY bridge pass; child exits |
| Frontend gate |
npm --prefix frontend run check:quality |
exit 0 |
| Full gate |
npm run check && npm run test:integration |
all pass |
| Release bundle |
npm --prefix frontend run desktop:dist |
production artifacts created |
Scope
In scope:
frontend/scripts/desktop-package-smoke.mjs (create)
frontend/scripts/desktop-package-smoke.test.mjs (create)
frontend/package.json
frontend/package-lock.json only if a test dependency is unavoidable
.github/workflows/ci.yml
frontend/desktop/electron-builder.yml only to expose a deterministic
unpacked output path or required environment, not to change release identity
Out of scope:
- UI automation beyond startup and bridge health.
- Code signing/notarization validation or publishing release assets.
- Using a developer's canonical user data, controller, or model installation.
- Replacing the existing
afterPack guard.
- Cross-platform matrix beyond the macOS runner currently used for release; add
other platforms only when packaging support exists.
Solution design
Create a dependency-free Node harness that resolves the current platform's
unpacked executable, creates a temporary user-data directory, allocates a CDP
debug port, and spawns the packaged app with auto-update disabled and all
runtime/data paths isolated. Wait for embedded-frontend.port, request
http://127.0.0.1:<port>/api/desktop-health, and require HTTP 200 plus the
documented healthy JSON. Connect to the BrowserWindow through CDP and evaluate
window.localStudioDesktop.terminal.status(); require available: true to prove
the packaged native module loads. Quit gracefully, escalate termination only
after a timeout, verify no app/embedded-server child remains, and always remove
temporary state.
Implementation plan
Step 1: Build a unit-tested lifecycle harness
Create focused pure helpers for output-path resolution, bounded file polling,
health validation, CDP target selection, and child cleanup. Inject spawn, fetch,
clock, and filesystem dependencies in unit tests. Do not add comments; use named
functions to express intent.
Verify: node --test frontend/scripts/desktop-package-smoke.test.mjs → success, timeout, malformed health, early exit, and cleanup cases pass.
Step 2: Launch the real packaged app in isolation
Implement desktop-package-smoke.mjs with a unique temporary user-data path,
LOCAL_STUDIO_DESKTOP_DISABLE_AUTO_UPDATE=true, no dev-server URL, an isolated
agent cwd/data directory, and a hard global deadline. Capture bounded stdout and
stderr for failure diagnostics without printing environment variables.
Verify: npm --prefix frontend run desktop:pack && npm --prefix frontend run desktop:smoke → exits 0 and removes its temporary directory and child processes.
Step 3: Assert embedded health and the native PTY bridge
Read the persisted port only from the temporary user data. Validate the desktop
health payload, then use CDP Runtime.evaluate with awaited promises to call the
preload terminal status API. Fail if the bridge is absent, rejects, or reports
the native module unavailable. Do not open a shell in CI.
Verify: deliberately point the harness at a copied bundle missing the native PTY artifact in a unit fixture → smoke fails with a redacted actionable error; the normal bundle passes.
Step 4: Add a macOS CI packaging gate
Add a macOS job to .github/workflows/ci.yml after source gates. Build the
unpacked app for the runner architecture and run desktop:smoke. Preserve Plan
001 release dependencies so release waits for this job; preserve audit proposal 017 pins
and frozen installs. Upload bounded logs only on failure, never user data.
Verify: workflow pin validation passes and the release job's needs includes the new desktop smoke job.
Step 5: Run full validation and canonical reinstall
Run tests and both desktop build modes, then use the exact AGENTS.md sequence
to replace /Applications/Local Studio.app, relaunch, and query its recorded
/api/desktop-health endpoint.
Verify: node --test frontend/scripts/desktop-package-smoke.test.mjs && npm --prefix frontend run desktop:pack && npm --prefix frontend run desktop:smoke && npm --prefix frontend run check:quality && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist → all exit 0; installed desktop health returns HTTP 200 with ok: true.
Test plan
- Unit lifecycle: successful startup, child exits before port, missing port,
timeout, malformed port, non-200 health, invalid JSON, unhealthy JSON, missing
preload bridge, unavailable PTY, and cleanup after every failure.
- Real unpacked app: embedded standalone server, static assets, desktop health,
preload bridge, and packaged native PTY load.
- Assert temporary data/log paths are removed and no recorded PID remains alive.
- Ensure smoke never contacts update servers or a production controller.
Acceptance criteria
Risks and stop conditions
Stop and report if:
- The app lacks a supported user-data override or still touches canonical state.
- CDP cannot be enabled only for the smoke process without changing production
security defaults.
- Native PTY availability cannot be queried without opening an interactive shell.
- CI architecture cannot produce a runnable unpacked app.
- A release would need to be published to test the flow.
Maintenance considerations
Keep the smoke below a fixed timeout and preserve failure logs as bounded
artifacts. Reviewers should verify it launches dist-desktop, not electron .
or a dev server. Any new packaged native dependency should gain a bridge-health
assertion in this harness.
Generated from Local Studio 2.0 main-branch audit proposal 030.
Audit metadata
07be5be7ce69bea0c3118744ab90d148b010fce0(origin/mainon 2026-07-17)Dependencies
Description and impact
The existing desktop post-pack check proves that one embedded server file was
copied, but it does not launch the packaged app. Packaging can still ship an app
whose embedded Next server fails, whose native PTY module cannot load, or whose
health endpoint never becomes reachable. A bounded end-to-end smoke on the
unpacked production bundle catches those failures before release assets are
created.
Current state
frontend/package.json:29-38defines packageddesktop:packand releasedesktop:distbuilds.frontend/desktop/electron-builder.yml:7-58selects app files, native PTYmodules, embedded standalone resources, targets, and
afterPack.frontend/scripts/electron-builder-after-pack.mjs:30-50asserts only that anested or flat
server.jsexists.frontend/desktop/logic/app-server.ts:38-64persists the embedded frontendport in
embedded-frontend.port; startup waits for the server before showingthe app.
frontend/desktop/preload.ts:18-24exposeslocalStudioDesktop.terminal.status;that IPC call loads
@lydell/node-ptyand reports availability.environment setting; use both so CI cannot touch developer state or update.
Verification commands
npm --prefix frontend run desktop:packnpm --prefix frontend run desktop:smokenpm --prefix frontend run check:qualitynpm run check && npm run test:integrationnpm --prefix frontend run desktop:distScope
In scope:
frontend/scripts/desktop-package-smoke.mjs(create)frontend/scripts/desktop-package-smoke.test.mjs(create)frontend/package.jsonfrontend/package-lock.jsononly if a test dependency is unavoidable.github/workflows/ci.ymlfrontend/desktop/electron-builder.ymlonly to expose a deterministicunpacked output path or required environment, not to change release identity
Out of scope:
afterPackguard.other platforms only when packaging support exists.
Solution design
Create a dependency-free Node harness that resolves the current platform's
unpacked executable, creates a temporary user-data directory, allocates a CDP
debug port, and spawns the packaged app with auto-update disabled and all
runtime/data paths isolated. Wait for
embedded-frontend.port, requesthttp://127.0.0.1:<port>/api/desktop-health, and require HTTP 200 plus thedocumented healthy JSON. Connect to the BrowserWindow through CDP and evaluate
window.localStudioDesktop.terminal.status(); requireavailable: trueto provethe packaged native module loads. Quit gracefully, escalate termination only
after a timeout, verify no app/embedded-server child remains, and always remove
temporary state.
Implementation plan
Step 1: Build a unit-tested lifecycle harness
Create focused pure helpers for output-path resolution, bounded file polling,
health validation, CDP target selection, and child cleanup. Inject spawn, fetch,
clock, and filesystem dependencies in unit tests. Do not add comments; use named
functions to express intent.
Verify:
node --test frontend/scripts/desktop-package-smoke.test.mjs→ success, timeout, malformed health, early exit, and cleanup cases pass.Step 2: Launch the real packaged app in isolation
Implement
desktop-package-smoke.mjswith a unique temporary user-data path,LOCAL_STUDIO_DESKTOP_DISABLE_AUTO_UPDATE=true, no dev-server URL, an isolatedagent cwd/data directory, and a hard global deadline. Capture bounded stdout and
stderr for failure diagnostics without printing environment variables.
Verify:
npm --prefix frontend run desktop:pack && npm --prefix frontend run desktop:smoke→ exits 0 and removes its temporary directory and child processes.Step 3: Assert embedded health and the native PTY bridge
Read the persisted port only from the temporary user data. Validate the desktop
health payload, then use CDP
Runtime.evaluatewith awaited promises to call thepreload terminal status API. Fail if the bridge is absent, rejects, or reports
the native module unavailable. Do not open a shell in CI.
Verify: deliberately point the harness at a copied bundle missing the native PTY artifact in a unit fixture → smoke fails with a redacted actionable error; the normal bundle passes.
Step 4: Add a macOS CI packaging gate
Add a macOS job to
.github/workflows/ci.ymlafter source gates. Build theunpacked app for the runner architecture and run
desktop:smoke. Preserve Plan001 release dependencies so release waits for this job; preserve audit proposal 017 pins
and frozen installs. Upload bounded logs only on failure, never user data.
Verify: workflow pin validation passes and the release job's
needsincludes the new desktop smoke job.Step 5: Run full validation and canonical reinstall
Run tests and both desktop build modes, then use the exact
AGENTS.mdsequenceto replace
/Applications/Local Studio.app, relaunch, and query its recorded/api/desktop-healthendpoint.Verify:
node --test frontend/scripts/desktop-package-smoke.test.mjs && npm --prefix frontend run desktop:pack && npm --prefix frontend run desktop:smoke && npm --prefix frontend run check:quality && npm run check && npm run test:integration && npm --prefix frontend run desktop:dist→ all exit 0; installed desktop health returns HTTP 200 withok: true.Test plan
timeout, malformed port, non-200 health, invalid JSON, unhealthy JSON, missing
preload bridge, unavailable PTY, and cleanup after every failure.
preload bridge, and packaged native PTY load.
Acceptance criteria
desktop:smokelaunches the actual unpacked production app./api/desktop-healthreturns 200 and healthy JSON.Risks and stop conditions
Stop and report if:
security defaults.
Maintenance considerations
Keep the smoke below a fixed timeout and preserve failure logs as bounded
artifacts. Reviewers should verify it launches
dist-desktop, notelectron .or a dev server. Any new packaged native dependency should gain a bridge-health
assertion in this harness.
Generated from Local Studio 2.0 main-branch audit proposal 030.