Add deterministic Electron E2E and harden local model attachments - #19
Conversation
…fore spawn (macOS posix_spawnp fix)
node-pty 1.1.0's npm prebuilt tarball restores spawn-helper without its
execute bit (0644). posix_spawn of a non-executable file is exactly what
surfaces to users as 'posix_spawnp failed.' the first time they open the
terminal drawer.
TerminalService.ensureSpawnHelperExecutable now resolves every prebuilds/*
helper (including app.asar.unpacked), chmods only when the execute bit is
missing, and verifies afterward — throwing a path-bearing remediation error
instead of silently swallowing (the prior catch {}). Shell resolution now
verifies the candidate is executable with a fallback chain (/bin/zsh ->
/bin/zsh -> /bin/bash -> /bin/sh) so a stale SHELL can no longer break
terminal creation.
Packaged builds must never ship spawn-helper without its execute bit. The afterPack hook now walks app.asar.unpacked/.../node-pty/prebuilds/*, chmods each spawn-helper to 0755, and throws in CI if any is missing or still non-executable — so a broken package fails at build time rather than at the user's first terminal open.
…ation Adds TerminalHistoryStore: a debounced, line-capped, per-workspace log of terminal output rooted at <userData>/terminal-history. Output is sanitized before persisting so a replayed snapshot cannot trigger fresh shell replies — CSI cursor-position reports, device-attributes/status queries, DECRQM/PM, XTVERSION, Kitty keyboard, DCS DECRQSS/XTGETTCAP, and OSC color queries are stripped while benign SGR/cursor sequences survive. Partial sequences split across chunks are carried via a pending prefix. Ported from t3code's sanitizeTerminalHistoryChunk (Manager.ts:953).
Shell fallback: the spawn path now walks an executable candidate list ($SHELL -> /bin/zsh -> /bin/bash -> /bin/sh) and retries the next on a retryable failure (posix_spawnp failed, ENOENT, not found). A broken $SHELL self-heals instead of throwing. Non-retryable errors (EINVAL, out of fds) surface immediately. The session result gains resolvedShell and preferredShellSkipped so the renderer can tell the user which shell launched. History wiring: TerminalService now accepts an optional historyStore. On open the prior sanitized output seeds the buffer (the renderer re-hydrates xterm from snapshot, so no renderer change is needed for the seed); each PTY data event appends to the store; terminate/exit flush the final chunk.
…hell refactor The phase3 contract test asserts the terminal.ts source orders revalidate → abort-check → spawn → abort-check. The spawn call changed shape (single spawn → trySpawnShell destructure) in the shell-fallback PR; update the assertion to match while preserving the ordering invariant it protects.
…history feat(terminal): shell fallback retry + persisted sanitized history
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — 47 files adding deterministic E2E Electron tests, hardening attachment ingestion with process-owned admission and raster signature validation, migrating released onboarding provider identities, and wiring drag/drop + clipboard image attachment through a fixed-purpose preload bridge.
- E2E harness — 6 deterministic Playwright Electron workflows against a test-owned mock LM Studio server, hermetic environment isolation validated at launch, CI + release gating.
- Attachment admission —
AttachmentIngestionAdmissionenforces per-document serialization and global concurrency/byte budgets. Picker, drop, and clipboard paths all route throughrunOwnedAttachmentIngestionwithisActiveguards. - Raster validation —
imageBytesMatchMimeverifies PNG/JPEG/GIF/WebP/BMP/HEIC/HEIF file signatures. New appends reject SVG and mismatched MIME/bytes; stored/legacy mode preserves backward compat. - Preload bridge —
createAttachmentPreloadBridgeuses onlywebUtils.getPathForFilefor drop paths; clipboard payloads are validated before IPC transit. - Onboarding — local providers are discovered and tested before saving; model cache and selection are persisted inline. Focus target class prevents stale button refs after editor close.
- Provider migration —
custom:onboarding-lmstudio/custom:onboarding-ollamamigrate to canonical IDs with collision-safe numeric siblings. Recognized byisLmStudioProviderId/isOllamaProviderIdregex. - Model discovery — Ollama tags + detail requests share one
AbortSignalwith bounded timeout. LM Studio response surfacesrecommendedModelfrom first loaded instance. - Composer — drag-over, drop, and paste handlers feed the fixed attachment bridge.
attachmentOperationRefguard withfinallycleanup prevents concurrent ingestion. - Contract tests — 14 new/updated source-reading and IPC-contract tests enforce channel allowlists, preload key shapes, exact-key parsing, and raster admission rules.
- Unit + integration tests — attachment admission (per-doc, global count, representation budget), clipboard payloads (detached bytes, too-large, mismatch-signature), Ollama timeout shared signal, onboarding identity migration, icon resolution for numeric siblings, Google provider localStorage migration through aliases.
DeepSeek Pro | 𝕏
fix(terminal): resolve 'posix_spawnp failed.' on macOS terminal creation
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — 8 new commits adding terminal shell retry/fallback, a per-workspace history store with control-sequence sanitization, build-time and runtime spawn-helper executable fixes, a renderer toast for fallback shells, and IPC contract additions.
- Shell retry loop —
trySpawnShelltries ordered candidates ($SHELL,/bin/zsh,/bin/bash,/bin/sh); retryable failures (e.g.posix_spawnp failed) fall through to the next candidate; non-retryable errors (EINVAL) surface immediately. - Spawn-helper executable fixes — runtime
ensureSpawnHelperExecutablenow enumerates all prebuild dirs and.asar.unpackedmirrors; build-timemakeSpawnHelpersExecutableinconfigureElectronFusesruns atafterPack. Both paths chmod + verify with descriptive errors. - Terminal history store —
TerminalHistoryStorepersists per-workspace sanitized output to disk with debounced writes;sanitizeTerminalHistoryChunkstrips device-query/reply sequences (CSI/DCS/OSC) across chunk boundaries while preserving benign SGR colors. - IPC contract —
TerminalSessiongainsresolvedShellandpreferredShellSkipped;TerminalHistoryStoreLikestructural interface keeps the service testable. - Renderer toast —
terminal-drawer.tsxsurfaces a one-time toast when a fallback shell launched the terminal. - Tests — 27 additional unit tests for shell retry, spawn-helper chmod/verify, history sanitization, round-trip, isolation, clear, and debounce coalescing. 5 additional build-script tests for
makeSpawnHelpersExecutable. All pass.
ℹ️ Deferred wiring observation
The TerminalHistoryStore class and all its wiring hooks in TerminalService (onData, onExit, terminate) are complete and tested, but the store is not yet activated in production: the terminalService singleton is new TerminalService() with no historyStore option, and TerminalHistoryStore.create() is never called. The feature is fully implemented — the ?. guards ensure no crash — but has no runtime effect until explicitly wired.
Technical details
# Deferred wiring
## Affected sites
- `main/services/terminal-history.ts:284` — `static async create()` is never called in production
- `main/services/terminal.ts:519` — `new TerminalService()` produces a service with no `historyStore`
- `main/services/terminal.ts:317,330,403` — all three hooks use `?.` guards correctly
## Required outcome
- Call `TerminalHistoryStore.create()` at app startup and pass it to the `TerminalService` constructor, or confirm the infrastructure-first landing is intentional and wiring will follow in a separate PR.
## Open questions for the human
- Should the `pending` map in `TerminalHistoryStore` evict entries for workspaces that haven't seen a terminal in some time window? Currently `flush()` persists but doesn't remove the in-memory entry, and `clear()` is never called from production code.DeepSeek Pro | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the incremental delta resolves the deferred wiring observation from the prior review and hardens the history store's internal state management.
- Production wiring —
TerminalHistoryStoreis created atapp.whenReady()and installed intoterminalService; aflushHistory()call is added toshutdownAndQuit. This directly addresses the prior review's observation that the store had no runtime effect. - Character-bound cap —
capHistorynow enforcesMAX_HISTORY_CHARS(200,000) as a hard upper bound alongside the 5,000-line bound. Histories without line breaks are no longer unbounded. - Revision tracking —
PendingWorkspaceStategainsrevision,persistedRevision, andwriteInFlightfields.persist()serializes writes per workspace and reschedules a debounced write when new output arrives during an in-flight write. flush()rewrite — uses a retry loop that cancels scheduled writes, callspersist(), and repeats until all revisions are caught up.flushAll()flushes all workspaces concurrently for shutdown.read()state initialization — newly populates thependingmap, so aread()+append()+flush()sequence works without an explicit firstappend().- Onboarding copy — the terminal feature bento description now mentions sanitized local history.
- Tests — 5 new tests: character-bound cap, restart-append, flushAll settling, flushAll write-race, and a source-reading production-wiring assertion.
DeepSeek Pro | 𝕏

Summary
Why
The app had no repeatable application-level Electron acceptance gate. Local-provider onboarding could save stale identities or choose incomplete catalogs, and drag/drop or clipboard attachments lacked a safe end-to-end bridge. The new suite reproduces those workflows without requiring LM Studio on GitHub runners, while the production changes close the provider-migration, attachment-race, and payload-validation gaps found during three independent final audits.
CI and LM Studio
The default
npm run test:e2esuite starts a test-owned LM Studio-compatible server on a random loopback port. GitHub CI does not need LM Studio installed, a downloaded model, or port 1234.Real visual inference remains opt-in through
npm run test:e2e:live:lmstudioand is not part of CI or release workflows.Validation
npm testnpm run test:e2e— 6/6 deterministic Electron workflowsnpm run test:e2e:live:lmstudio— live vision model read a per-run image tokennpm run type-checknpm run type-check:e2enpm run lintnpm audit --audit-level=low— 0 vulnerabilitiesgit diff --checknpm run packagenpm run package:verifyReview notes
The branch contains 47 commits for 47 changed files, with exactly one file per commit. The pre-integration local safety stash remains untouched.