feat: @wdio/electrobun-service — Electrobun desktop testing support#314
feat: @wdio/electrobun-service — Electrobun desktop testing support#314goosewobbler wants to merge 28 commits into
Conversation
Release Preview — no release
Updated automatically by ReleaseKit |
…iring Bootstrap the Electrobun desktop testing service (ROADMAP Phase 5) as a CDP-attach archetype, cloned from the Electron service pair. This is the Foundation PR of a 4-PR stack (Foundation → MVP → Feature-complete → Ship) that merges into the feat/electrobun-service integration branch. - native-types: add src/electrobun.ts (ElectrobunServiceAPI, options, mock, capabilities as a plain interface, browser extension, window.__WDIO_ELECTROBUN__) and wire the WebdriverIO Browser / MultiRemoteBrowser / Capabilities / ServiceOption augmentation. - @wdio/electrobun-cdp-bridge: skeleton for the multi-target CDP client — constants (DEFAULT_PORT=9222 + the CEF 9222–9232 auto-select range), types (Debugger/Version + PageTarget/TargetClass/TargetRegistryEntry), barrel, README, export-shape test. Connection manager lands in MVP. - @wdio/electrobun-service: skeleton mirroring @wdio/dioxus-service — launcher extends BaseLauncher (handles browser mode; native-mode spawn + CEF port discovery land in MVP), worker service stub, errors (cefRendererRequired / deeplinkUnsupportedOnPlatform), constants, serviceConfig, session-free index, unit tests. - turbo.json: build-graph entries for both new packages. Decision baked in: CDP only works under Electrobun's CEF renderer, so the service requires apps built with defaultRenderer:'cef' on all three OSes; WebKit-default apps are a documented unsupported gap. Verification: typecheck + unit tests green for both packages (bridge 3/3 @100% cov, service 10/10), Biome clean, no regressions to native-types. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture the Phase 0 spike results in agent-os/specs (the tracked research-findings convention). A throwaway CEF Electrobun app was built and driven end-to-end on macOS, confirming the CDP-attach + Chromedriver debuggerAddress model is viable. Key results that refine PR2: - Port is PINNABLE — --remote-debugging-port overrides CEF's 9222-9232 scan. - CEF is single-instance per --user-data-dir → parallel workers need distinct user-data-dirs + pinned ports (biggest MVP item). - Each window/view = one CDP page target (no shell); mock seam is Page.addScriptToEvaluateOnNewDocument over window.__electrobun.*. - Chromedriver matches on major 147; deeplink works on macOS via open-url. - Still unverified: Linux CDP (remote_debugging_port commented out) + Windows. - Pin a known-good Electrobun release; 1.18.4-beta.3 ships two build defects. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- launcher: anchor PortManager ports clear of CEF's [9222, 9232] auto-scan range (baseNativePort was 9223, inside the range). Electrobun is CDP-attach with no native driver, so baseNativePort is nominal — added DEFAULT_DEBUG_PORT_BASE (9333) as the allocation anchor. [P1] - launcher: drop the full JSON.stringify(config/capabilities) debug dumps — the testrunner config can carry reporter tokens / cloud credentials that shouldn't reach debug logs. [P2] - cdp-bridge + service constants: correct the misleading "discovers by scanning rather than dictating" port comments — the launcher pins the port per worker by writing build.json; the 9222-9232 scan is CEF's fallback only. [P1] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ot discovered The remoteDebuggingPort JSDoc still described port auto-discovery (a stale model); the launcher allocates + pins a port per worker into the bundle's build.json. Reword so users configuring parallel/multiremote runs aren't misled — each worker gets a distinct auto-allocated, pinned port; this option only forces a fixed one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source investigation of the Electrobun macOS native wrapper resolving the spike's blocker 1 (can the debug port + cache dir be set at launch?): - Port IS launch-overridable: CefMainArgs is built from the process argv, so --remote-debugging-port=N reaches CEF's command line and wins over the 9222-9232 scan. Plan: build without pinning the port; launcher passes a distinct allocated port per spawn. - Cache/user-data dir is NOT launch-overridable: root_cache_path is keyed on identifier+channel set via FFI from the Bun launch context, and CEF ignores --user-data-dir once root_cache_path is set. Same-app instances share the cache root → CEF folds the 2nd launch into the 1st. Implication: multiremote/parallel is blocked pending an upstream Electrobun change (cache-root/channel override at launch) — the same shape as Dioxus's Linux-external gap. MVP ships single-instance; multiremote documented as a known limitation + upstream tracking issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the core of the multi-target CDP client (the novel piece vs the single-target electron-cdp-bridge): - devTool.ts — /json + /json/version discovery; the consumer keeps ALL page targets (CEF exposes one per webview), not just the first. - connection.ts — one CDP WebSocket per target (devtools-protocol-typed send, promise map, EventEmitter). Exposes NO navigate helper — attaching is observation-only. - targetRegistry.ts — pure classify (views:// content vs shell/other) + stable labelling (main, window-1, …) keyed on the CEF target id, monotonic counter so closed windows don't reclaim labels. - bridge.ts — CdpBridge: discovers + reconciles targets, holds one connection per attached target, routes send()/sendTo()/switchTarget()/listWindows(), and enables only Runtime on attach. Invariant: never issues Page.navigate. Tests: classify/label (incl. stability + no stale reclaim) and a CdpBridge Page.navigate-never-sent guarantee. 18 tests, typecheck + Biome clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… CFFIXED_USER_HOME The CFFIXED_USER_HOME workaround test corrects the prior launch-time investigation and resolves multiremote: - CORRECTION: the --remote-debugging-port launch arg does NOT work. The earlier source-inference (CefMainArgs gets process argv) was refuted empirically — the shipped main.js launcher never forwards argv to CEF. The port is read only from Contents/Resources/build.json chromiumFlags.remote-debugging-port. To vary the port per worker, clone the .app (APFS cp -c) and write the port into the clone's build.json. - CFFIXED_USER_HOME per instance redirects CEF's cache root (via CFCopyHomeDirectoryURL), defeating single-instance folding. Verified: 2 concurrent same-app instances both served CDP and were independently driveable (Runtime.evaluate 1+1 -> 2 on both ports, 2 process trees, 2 caches). Result: multiremote/parallel IS achievable on macOS with NO upstream change — per worker = clone .app + distinct build.json port + distinct CFFIXED_USER_HOME. MVP (PR2) ships single-instance (N=1); multiremote (PR3) is the same path N>1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lpers Pure/IO helpers to locate and verify a built CEF Electrobun app and to read/write the CEF remote-debugging port the launcher pins into the bundle: - resolveElectrobunApp: require an explicit appBinaryPath (no auto-detection), resolve binary/bundle/resources/build.json/identifier. macOS accepts the .app dir or the inner binary; Windows/Linux are best-effort (sibling build.json) pending layout verification. - verifyCefRenderer: macOS passes on a bundled CEF framework or a cef build.json marker, else throws cefRendererRequired; non-macOS is best-effort and never false-negatives. - readBuildJson / getRemoteDebuggingPort / writeRemoteDebuggingPort: parse and pin chromiumFlags["remote-debugging-port"] (string), preserving other keys. Unit-tested with temp .app fixtures covering resolution, the CEF pass/fail matrix, and the port read/write round-trip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the launcher's native-mode path (browser mode unchanged): - onPrepare: resolve + CEF-verify each app bundle, force browserName='chrome', store resolved app info for onWorkerStart. Propagates the missing-path and cefRendererRequired SevereServiceErrors. - onWorkerStart: allocate a CDP port via BaseLauncher.portManager, pin it into the bundle's build.json (port is fixed per bundle, not a launch arg — TODO PR3 clones the bundle per worker instead of mutating in place), spawn the app via the new nativeMode module, and set goog:chromeOptions.debuggerAddress. - onComplete: stop spawned apps (kill + remove temp CFFIXED_USER_HOME) then the existing stopAllDrivers + closeLogWriter. New nativeMode.ts owns the spawn: per-run CFFIXED_USER_HOME temp dir for an isolated CEF cache root, optional backend stdout/stderr capture via @wdio/native-core createLogCapture, and SIGTERM→SIGKILL teardown. Adds `env` to the Electrobun option types so callers can pass extra spawn env. Unit tests mock the local config + nativeMode seams (no real process/bundle) and cover the browser/native onPrepare matrix incl. every SevereServiceError throw, port pinning + debuggerAddress wiring, and teardown. The live spawn/attach path is an E2E-validation gap (no built CEF bundle in unit tests) — noted in code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion Worker service before() attaches a CdpBridge to the CEF debugger endpoint read from goog:chromeOptions.debuggerAddress (set by the launcher) and installs the browser.electrobun.* surface: - execute: real — runs the user function (or raw expression) in the active CEF content target via the bridge's Runtime.evaluate (returnByValue + awaitPromise), args inlined as JSON literals, first callback arg is window.__WDIO_ELECTROBUN__. - switchWindow / listWindows: real — delegate to bridge.switchTarget / listWindows. - mock / clearAllMocks / resetAllMocks / restoreAllMocks / isMockFunction / triggerDeeplink: stubs that throw a clear not-implemented error (triggerDeeplink uses deeplinkUnsupportedOnPlatform off macOS). - Browser mode (devServerUrl) skips CDP attach. Multiremote attaches per instance. - after()/afterSession() close bridges defensively (benign-error tolerant). New session.ts mirrors the dioxus session API (createElectrobunCapabilities / init / cleanup) over the CDP-attach flow: init() drives launcher onPrepare + onWorkerStart, opens the Chromedriver session, runs service.before(); cleanup() runs worker teardown, deletes the session, and calls onComplete. Wired into the index re-exports as startWdioSession / cleanupWdioSession. Tests mock the CdpBridge / launcher / service / remote() seams. The live CDP attach + Runtime.evaluate round-trip is an E2E-validation gap (no built CEF bundle in unit tests) — noted in code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Searched blackboardsh/electrobun — no existing issue covers a runtime remote-debugging-port + cache/user-data-dir override for running concurrent instances (the clean unblock for WDIO multiremote). Adjacent issues: #445 (remote-debug opt-in), #380 (custom CEF cache paths), #227 (single-instance lock — inverse need), #228 (batteries-included E2E). Drafted a focused feature request; on hold (maintainer chose not to file yet) — kept on record to revisit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- electrobun-cdp-bridge/devTool: fix the `.catch().then()` chain in #executeRequest — the `.then()` ran even after the port-wait rejected (catch resolves), firing an HTTP request at a port that never opened. Reordered to `.then(req).catch(reject TIMEOUT)`. [P2] - electrobun-service/execute: reject function/symbol args explicitly. JSON.stringify returns undefined (not a throw) for those, so they were silently dropped into the inlined script as `undefined`. [P2] - Test: cover function/symbol arg rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n refresh Greptile (PR #311): refresh() removed dead connections but left #activeLabel pointing at a closed window, so every later send()/on() threw an opaque NOT_CONNECTED. Now auto-advance to the first surviving target (or undefined if none) when the active label is pruned; callers can still switchTarget() explicitly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lose cleanup Greptile (PR #311 review summary, non-functional style points): - send(): the per-request timeout timer was never cleared once the response arrived, leaving a dangling timer until it fired. Track the timer on the promise handler and clearTimeout it in the response handler + bulk reject. - #close(error): it called #rejectAllPromises(error) AND ws.close() then triggered the 'close' handler which rejected again (redundant). Route rejection through the single 'close' handler via #closeReason (rejecting directly only when there's no open socket / no 'close' event coming). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the triggerDeeplink stub with a real implementation: on macOS, validate the URL (rejects http/https/file) and fire the OS-native protocol handler via @wdio/native-core's deeplink helpers (`open <url>`) so the app's registered open-url handler receives it — the production code path. Non-macOS throws the documented-gap error (Electrobun deeplinks are macOS-only upstream today). - New src/commands/triggerDeeplink.ts (mirrors the dioxus command, macOS-gated). - service.ts installApi wires the real command (drops the stub + now-unused deeplinkUnsupportedOnPlatform import; it's used inside the command). - New test/triggerDeeplink.spec.ts (mocks executeDeeplinkCommand — no real spawn; covers macOS-fires / rejects-http / non-macOS-throws). - Removed the stale service.spec stub assertion (which also fired a real `open`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author the source for two committable Electrobun fixtures (source only — building is a later CI/PR4 concern): - fixtures/e2e-apps/electrobun: full E2E fixture. CEF renderer enabled on all three OSes (only CEF exposes the CDP endpoint the service attaches to). Two views/windows (mainview counter + secondview) exercise the multi-window switchWindow/listWindows surface. The Bun backend registers an open-url handler that surfaces the deeplink URL into the main view's DOM (window.__wdioDeeplinks + #status) for triggerDeeplink tests. - fixtures/package-tests/electrobun-app: reduced install-smoke fixture — single CEF window, big-glass styling, #app-title + #status only. Both views use the big-glass purple-gradient visual template shared with the Tauri fixture, with stable selectors (#app-title, #counter, #increment-button, #decrement-button, #reset-button, #status). electrobun is pinned to an exact published version (1.18.1) rather than the spike's local file: link. package-test deps are explicit exact versions (no catalog refs) since those run in isolated installs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add fixtures/e2e-apps/electrobun and fixtures/package-tests/electrobun-app to the pnpm-workspace.yaml packages list (mirroring the dioxus entries) and refresh the lockfile with the electrobun@1.18.1 resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each worker now gets its own bundle clone with its own pinned CEF remote-debugging port, instead of mutating the shared bundle's build.json in place. CEF reads the port only from the bundle's build.json (not a launch arg) and is single-instance per cache root, so concurrent same-app instances need both a private bundle copy and a distinct CFFIXED_USER_HOME. - nativeMode: add cloneAppBundle (APFS `cp -Rc` clonefile on darwin, recursive cpSync fallback / non-darwin); spawnElectrobunApp now takes the ResolvedElectrobunApp, clones it, rebases the binary + build.json onto the clone, pins the port into the clone, and spawns the cloned binary. ElectrobunAppProcess tracks cleanupDirs (user home + clone parent); teardown removes both, tolerant of failures. - launcher: drop the in-place writeRemoteDebuggingPort call (clone + port-write now happen inside the spawn path); spawnApp seam passes the resolved app through. - tests: cover the darwin clonefile path, the cpSync fallback, the non-darwin path, the missing-build.json guard, port-pinned-into-clone, cloned-binary spawn, dual-dir teardown, and per-capability distinct ports for multiremote. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(PR3)
Replace the throwing mock stubs in the worker service with a real
mocking surface modelled on Electron's browser-mode mock (in-page
recorder over CDP), satisfying the already-defined ElectrobunMock /
ElectrobunServiceAPI types.
- mock(target): target is a dotted path to a function in the webview
global scope ('api.fetchData' -> window.api.fetchData) — the in-page
analogue of dioxus/tauri's string-target mock, since Electrobun has no
enumerable main-process API.
- Inner recorder (innerRecorder.ts): injected at mock()-time via
bridge.send('Runtime.evaluate', ...) — never navigates. Walks the path,
wraps the target with a vitest-shaped spy under
window.__WDIO_ELECTROBUN_MOCKS__, preserves the original for restore,
and is idempotent (no double-wrap).
- Outer mock (mock.ts): a @wdio/native-spy fn()-backed ElectrobunMock.
update() reads inner call data back over CDP and syncs one-way into
mock.calls/results/invocationCallOrder. mockImplementation/return/
resolve/reject/returnThis (+Once) push behaviour into the inner spy,
reusing execute.ts's JSON-serialisation guard (rejects fn/symbol args).
clear/reset/restore apply to both inner and outer.
- mockStore.ts: per-installed-instance Map (multiremote-safe) backing
clearAllMocks/resetAllMocks/restoreAllMocks(prefix?) and isMockFunction.
- service.ts: installApi() wires the family onto a per-bridge store and
clears stores on teardown. execute.ts factors out evaluateInActiveTarget
+ jsonLiteral for reuse by the mock layer.
mockAll/class-mock are intentionally omitted (Electron-only).
Tests mock the CdpBridge boundary and run the emitted recorder JS in a
Node vm sandbox to prove install -> record -> read-back -> impl/clear/
reset/restore end-to-end. The real in-webview round-trip is only fully
proven against a CEF app (no CEF in unit tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nativeMode: clean up the cloned bundle temp dir if writeRemoteDebuggingPort or the home-dir mkdtemp throws (it isn't tracked for teardown until spawn returns, so a throw leaked the large CEF-bearing clone). + regression test. [P2] - innerRecorder: make the call-data read-back replacer circular/function-safe so a mockReturnThis (or any circular result value) can't throw "Converting circular structure to JSON" out of the page during update(). [P2] - mock: mockRestore now resets the outer spy (impl + history), matching vitest's restore-calls-reset semantics, instead of only clearing history. [P2] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pare Greptile (PR #310 review summary, "mixed-mode mutation"): the launcher applies a single mode to all caps — browser mode forced browserName:'chrome' on every cap (even native ones) if ANY cap was browser-mode. Fail fast with a clear SevereServiceError on a mixed set instead of silently mis-handling a cap, and require a consistent mode. Fixed in the final (feature-complete) launcher. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…one dir on copy failure Greptile (PR #312 review summary, minor error-path gaps): - mock.ts/innerRecorder: include `name` when serialising a rejected-value Error into the page and reconstruct it with `.name`, so mockRejectedValue(new TypeError()) keeps its type in the webview (was dropped → always "Error"). - nativeMode.cloneAppBundle: wrap the copy so the mkdtemp'd parent dir is removed if cp/cpSync throws (e.g. ENOSPC) — it isn't returned/tracked on failure, so it would otherwise leak. + regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL flagged 5 `js/bad-code-sanitization` alerts on the `${key}` interpolations
in innerRecorder.ts — the mock `target` is `JSON.stringify`'d into scripts the
worker evaluates over CDP. A mock target is only ever a dotted path of JS
identifiers (all the RESOLVE_PATH `.split('.')` walk can address), so restrict it
to that charset in pathLiteral(): the inlined value can then never contain a
quote, backslash, newline, or line/paragraph separator, making JSON.stringify a
provably-safe escaping into the JS-string context (and giving CodeQL a recognised
sanitiser guard). Invalid targets now reject with a clear error before any script
reaches the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odeQL)
The VALID_TARGET guard hardened the input but CodeQL's js/bad-code-sanitization
still flagged the `${key}` interpolations: a regex `.test()` guard isn't a
sanitiser it recognises, and JSON.stringify leaves U+2028/U+2029 (JS source-string
terminators) raw and doesn't neutralise `</script>`. Finish the escaping on the
JSON.stringify output — the escaping CodeQL recognises for a JS-source context.
No behavioural change (the guard already rejects targets that could contain these).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b97de7f to
f615045
Compare
* test(electrobun): add E2E suite for @wdio/electrobun-service
Mirror the tauri/dioxus E2E setup against the CDP-attach electrobun service:
- wdio.electrobun.conf.ts: services ['electrobun'], browserName 'chrome' +
wdio:electrobunServiceOptions, autoXvfb (Electron headless path, not the Wry
xvfb-run wrap). Resolves the built CEF .app under fixtures/.../build via glob,
overridable with ELECTROBUN_APP_PATH. TEST_TYPE selects standard/window/deeplink.
- test/electrobun/: api, application (counter UI), execute, execute-data-types,
window (switchWindow/listWindows across mainview + secondview, labels
main/window-1), logging (Bun backend capture), mock (window.<target> spies),
deeplink (wdio-electrobun scheme, macOS-only). it('should …') throughout.
- e2e/package.json: link @wdio/electrobun-service + test:e2e:electrobun[:window|
:deeplink] scripts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): wire E2E build + test jobs (no Rust crates)
Mirror the dioxus CI gating for the CDP-attach electrobun service (no -crates
step — Electrobun has no Rust):
- _ci-build-electrobun-e2e-app.reusable.yml: build the CEF fixture bundle via
Bun (setup-bun) + `electrobun build`, upload build/ as an artifact. The beta
Bun/CEF toolchain (~150MB CEF download) is the biggest unknown here.
- _ci-e2e-electrobun-all-providers.reusable.yml: single-provider CDP-attach run
(no provider matrix); test-type standard/window/deeplink. autoXvfb manages
Xvfb on Linux (Electron headless path).
- _ci-detect-changes.reusable.yml: add run_electrobun output + electrobun_service
/ e2e_electrobun / fixtures_electrobun / infra_electrobun filters.
- ci.yml: electrobun-gated build + e2e jobs (if run_electrobun && !run_lint_only),
added to the ci-status gate.
Risk handling: macOS-ARM is the verified/required platform. Linux is a SEPARATE
build + e2e job marked continue-on-error (allow-failure) with a comment, since
the Linux CDP path (CEF serving /json) is unverified — and trivially disabled by
commenting out build-electrobun-e2e-app-linux + e2e-electrobun-linux. Fixture
pins electrobun@1.18.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): fix invalid continue-on-error on reusable-workflow jobs
`continue-on-error` is not permitted on a job that calls a reusable workflow via
`uses:` — GitHub rejected the whole CI run as an invalid workflow file (0 jobs
scheduled), so the electrobun build/e2e jobs never ran. Remove it from the two
Linux jobs and keep Linux non-blocking the valid way: exclude
build-electrobun-e2e-app-linux + e2e-electrobun-linux from `ci-status.needs`.
They still run for signal under their own gate; macOS-ARM stays required. Add
them back once the Linux CDP path is confirmed.
Also swap `ls -R` for `find` in the bundle-verify step (clears shellcheck SC2012).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): pin Bun + pick newest bundle by mtime (Greptile PR #316)
- Pin `bun-version: 1.3.14` (current latest, builds green) so a silent Bun bump
can't break the fragile beta CEF toolchain; bump deliberately.
- wdio.electrobun.conf.ts: when multiple .app bundles exist (dev/canary/stable),
pick the most-recently-built (mtime desc) instead of the lexicographically-first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): share CEF bundle as a tarball, not the zip/dist-target archive
The macOS e2e failed at "Verify Electrobun Bundle": the bundle never reached
fixtures/e2e-apps/electrobun/build. Root cause — the shared download-archive
action's extract heuristic only restores dist/dist-js/target dirs, never build/,
so the electrobun bundle (build/dev-macos-arm64/*.app) was extracted to a temp
dir and dropped. (zip -r would also resolve/duplicate the .app's CEF framework
symlinks.)
Share the bundle as a symlink-preserving tar.gz via actions/upload-artifact +
download-artifact instead, and untar into fixtures/e2e-apps/electrobun. The CEF
build itself is already green on macOS + Linux; this is purely the artifact
round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): don't let `find | head` SIGPIPE fail the bundle-verify step
The bundle now extracts correctly, but every e2e job still died at "Verify
Electrobun Bundle" — its last line `find …/build | head -50` closes the pipe
early, so under `set -o pipefail -e` the upstream `find` (thousands of CEF
entries) dies with SIGPIPE (141) and fails the step before any test runs.
Tolerate it (`-maxdepth 3 … | head -50 || true`). Unblocks the actual e2e run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): resolve only the top-level .app, not nested helper bundles
The real cause of the macOS e2e failure: `globSync('build/**/*.app')` also matched
the helper bundles nested inside the main app (Contents/Frameworks/bun Helper
(GPU).app, …). The mtime sort then resolved appBinaryPath to a helper, which has no
CEF framework, so verifyCefRenderer threw cefRendererRequired — on the wrong bundle.
Filter out any `.app` nested inside another `.app` so only the top-level app wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): spawn the inner launcher binary, not the .app directory
macOS e2e: onPrepare passed but every app spawn failed with EACCES (PID
undefined) → CEF never started → Chromedriver "chrome not reachable". Cause:
resolveMacosBinary guessed the exe is named after the bundle (`Foo.app` → `Foo`),
but Electrobun names its launch exe `launcher`; the guess missed and it fell back
to the `.app` directory, which isn't executable. Resolve the real binary via
Info.plist CFBundleExecutable, then `launcher`, then the bundle-name guess.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): pin chromedriver to CEF's Chromium major (147) for e2e
The app now launches and CEF starts, but Chromedriver refused to attach:
"session not created: This version of ChromeDriver only supports Chrome version
148". WDIO auto-fetched the latest driver (148); Electrobun 1.18.1 bundles CEF on
Chromium 147 (147.0.7727.118). Pin browserVersion: '147' so WDIO uses a major-147
driver — the spike confirmed a 147 driver attaches and drives the app
(RESEARCH_FINDINGS §2). Follow-up: have the service auto-detect CEF's Chromium
version (as electron-service matches Electron's) instead of pinning in the conf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun-cdp-bridge): default undefined options instead of overwriting
Object.assign({...defaults}, options) copies an explicit `undefined` over the
default. An unset service option (cdpConnectionRetryCount / cdpConnectionRetryInterval
/ cdpConnectionTimeout) therefore left timeout undefined (waitPort never waited →
immediate-fail) and connectionRetryCount undefined (`retries >= undefined` never
caps → effectively unbounded retries, ~10k attempts ms apart). Use per-field `??`
so undefined falls back to the default (10s timeout lets waitPort actually wait for
CEF to open the port; 3 bounded retries).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): create Library/Application Support in CFFIXED_USER_HOME
CEF builds its profile under $CFFIXED_USER_HOME/Library/Application Support/…, but
the per-worker home is a fresh mkdtemp dir lacking that structure; CEF won't create
the missing parents and fails ("Cannot create profile at path …"), so it never opens
the debugger port and target discovery times out. Pre-create the parent dir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): drop dead artifact_size/cache_key outputs + cache_key plumbing
When the build reusable switched from the shared zip upload-archive action to a
tarball + actions/upload-artifact, its artifact_size/cache_key outputs kept
pointing at steps.upload-archive.outputs.* — which no longer exist, so they were
empty stubs. Nothing consumes them now that the e2e job downloads the bundle by
artifact name, so remove: the two stub outputs, the leftover `id: upload-archive`
on the tar step, the e2e reusable's electrobun_cache_key input, and the two ci.yml
passthroughs. Keeps build_id/build_date (real, matches the sibling reusables).
Cleaner template before this pattern is copied to future framework workflows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): isolate CEF profile via --user-data-dir, not CFFIXED_USER_HOME
CEF couldn't create its profile under the redirected $HOME
($CFFIXED_USER_HOME/Library/Application Support/<id>/dev/CEF/partitions/default —
"Cannot create profile at path"), so it never opened the debugger port. Pin a
per-worker --user-data-dir into the clone's build.json chromiumFlags instead (CEF
reads flags there, not argv): a flat temp dir CEF can create, which also stops a
second launch folding into the first. Drops the CFFIXED_USER_HOME env + the
Library/Application Support pre-create.
Also pin e2e maxInstances=1 to rule out parallel-CEF contention while
single-instance CEF-on-CI is stabilised; raise once parallel runs are verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): wrap string execute scripts (support return/statements)
String-form execute evaluated the script as-is, so a statement-style script
(`return 42`, `const x = …; return x`, `return document.title`) failed with
"Illegal return statement". Wrap strings like @wdio/electron-service: leading
statement keyword or a top-level `;` → function body; otherwise treat as an
expression and return it. Bare expressions and IIFEs (nested `;` only) still
return their value. Reuses @wdio/native-utils' hasSemicolonOutsideQuotes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): tear down each worker's app in onWorkerEnd (no accumulation)
Apps were spawned per worker in onWorkerStart but only stopped in onComplete, so
they accumulated and ran concurrently for the whole run — multiple live CEF
instances contend on profile creation/resources, so only the first worker's app
came up and the rest timed out at session creation (even at maxInstances=1, since
specs are serial but apps stayed alive). Track spawned apps by worker cid and stop
them in onWorkerEnd per-spec; onComplete now sweeps any stragglers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): clean up user-data-dir when port pinning throws (Greptile #316)
userDataDir is created (mkdtemp) before writeRemoteDebuggingPort pins it into the
clone's build.json; if that pin throws (build.json missing/unwritable) the catch
only removed the clone, leaking the profile temp dir. Remove userDataDir too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): focus the WebDriver session on the content window
Chromedriver attaches to whatever CEF page lists first — often a blank shell
(about:blank) separate from the views:// content — so $/click/getText hit a blank
document (browsingContext.locateNodes returned []), while execute/mock worked via
the CdpBridge's content target. After attach, switch the WebDriver session to the
first non-blank window so element commands target the app by default. Best-effort
(logged, not fatal). Fixes application.spec's "element wasn't found" failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): fix invalid upload-artifact@v7 → @v4 (Greptile #316)
The "Upload Test Logs" step used actions/upload-artifact@v7 (no such major; latest
is v4), inconsistent with download-artifact@v4 in the same file and the build
workflow's upload-artifact@v4. Under continue-on-error it would fail silently,
dropping the e2e logs — exactly when they're most needed for the unverified CEF
path. Pin to @v4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): focus the bridge's main window + poll for fixture render
Two robustness fixes for the flaky e2e:
- The WebDriver window focus now targets the window matching the bridge's active
('main') target URL (not just the first non-blank one, which could be the
secondview), so $/click and execute/mock agree on the same content window.
- api.spec's DOM-read polls for #app-title via waitUntil instead of a single
read — the webview can still be painting when the bridge attaches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): sync the WebDriver window on switchWindow too (Greptile #316)
switchWindow only moved the CdpBridge's active target, so after
browser.electrobun.switchWindow('window-1') the Chromedriver session stayed on the
previous window — $('#second-marker')/$('#second-title') would query the wrong DOM.
Extract the window-alignment into syncWebDriverWindow (matches the bridge's active
target URL, falls back to first non-blank) and call it both after attach and after
every switchWindow, so $/click and execute/mock stay on the same window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun-cdp-bridge): deterministic main window (sort targets by URL)
The 'main' label went to whatever content target CEF's /json listed first, which
isn't stable — so 'main' (and thus execute, switchWindow, and the WebDriver focus)
could flip between mainview and secondview across runs, intermittently breaking
specs that drive the main window. Sort content targets by URL before labelling so
the primary window (views://mainview/…, sorts before secondview) is consistently
'main'. Fixes the application.spec regression from aligning focus to 'main'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): add exploratory Windows e2e build+test jobs (allow-failure)
Add Windows build + e2e jobs mirroring the Linux allow-failure pattern (excluded
from ci-status.needs, so non-blocking) to see whether the CEF toolchain builds and
attaches on Windows — the most promising next platform (Chromium/WebView2 are
CDP-capable). The conf currently globs *.app (macOS), so the Windows e2e is
expected to surface what's needed (build output layout, app-path resolution).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): --force-local so tar works on Windows (drive-letter paths)
The Windows build succeeded but the archive step failed: $RUNNER_TEMP is a drive
path (D:\a\_temp) and GNU tar reads `D:` as a remote host ("Cannot connect to D:").
Pass --force-local on Windows (only — macOS bsdtar lacks it) for both the archive
(build) and extract (e2e) tar calls, so the Windows bundle is produced/round-tripped.
electrobun build itself already works on Windows (downloads win-x64 core + bundles).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): address Greptile PR4 review (conf stat + CI cleanup match)
- wdio.electrobun.conf.ts: stat each globbed bundle once instead of inside the
sort comparator (which re-stat'd O(n log n) times), guard against a path that
races away between glob and stat, and drop the redundant statSync that ran
right after the existsSync check.
- CI cleanup: match the per-worker clone temp-dir prefix (wdio-electrobun-bundle-*)
in process argv rather than the .app display name. The macOS bundle keeps spaces
("WDIO Electrobun E2E-dev.app") and the Windows exe name drops them, so a
name-based match misses the CEF helper subprocesses; the temp-dir prefix is in
every spawned process's argv (launcher, bun child, CEF helpers) on both OSes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(electrobun): probe chrome-runtime profile flags for gated window/deeplink
Bounded CEF-on-CI experiment for the gated macOS window+deeplink legs. The CEF
chrome-runtime logs "Cannot create profile" for the persist:default partition
(BrowserWindow forces persist:default and doesn't expose `partition`; the
service's per-worker --user-data-dir lives outside electrobun's NSSearchPath
root_cache_path), so both webviews fall back to the shared global context and
the second window's renderer browser-info response times out. Add no-first-run
+ no-default-browser-check to see whether Chromium then initialises its profile
cleanly under automation. If this doesn't green the window+deeplink legs they
get gated as a documented upstream CEF gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Revert "test(electrobun): probe chrome-runtime profile flags"
The no-first-run / no-default-browser-check probe was inert: the window leg's
CEF log is identical (same "Cannot create profile" for partitions/default, same
browser-info timeout), and the flags regressed the previously-green standard
leg. The persist:default profile failure is an upstream CEF chrome-runtime
limitation (BrowserWindow forces a custom non-global partition the chrome
runtime can't create → global-context multi-browser race), not something a
chromiumFlag restructures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): gate macOS window+deeplink as allow-failure (upstream CEF gap)
Split e2e-electrobun-macos-arm: the `standard` suite (api/application/execute/
logging/mock) stays the required gate; window + deeplink move to a new
e2e-electrobun-macos-arm-advanced job, excluded from ci-status.needs like the
Linux/Windows legs. They still run for signal.
Both hit an upstream CEF chrome-runtime limitation: BrowserWindow forces a
persist:default partition the chrome runtime can't create as a non-global profile
("Cannot create profile at …/CEF/partitions/default"), so both webviews fall back
to the shared global context and the second window's renderer browser-info
response times out — a multi-browser-on-global-context race electrobun's own
native code documents. Not fixable from the fixture/service (BrowserWindow
exposes no partition; a chromiumFlags probe was inert). Re-fold into the required
job once electrobun makes the failed-profile fallback ephemeral-per-webview or
exposes a per-window partition.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): open the fixture's second window only for the window suite
The fixture opened a second CEF BrowserWindow unconditionally, forcing both
windows onto the persist:default partition the CEF chrome-runtime can't create as
a non-global profile. Both fall back to the shared global context and hit
electrobun's documented multi-browser race ("Timeout of new browser info
response") — which can break EITHER window, so api/application intermittently
failed on a non-rendering mainview, making the required `standard` leg flaky (it
passed only once, by luck).
Gate the second window on WDIO_ELECTROBUN_SECOND_WINDOW (set by the window-suite
conf, forwarded launcher→bun via the service `env` option). Single-window suites
(standard, deeplink) no longer create the race and render mainview reliably; the
window suite still opens both and remains gated allow-failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): skip multi-window + deeplink e2e (upstream CEF gap), keep standard
The window (multi-window) and deeplink suites can't run reliably on CI — an
upstream CEF per-instance profile-isolation limitation (BrowserWindow forces a
persist:default partition the chrome-runtime can't create as a non-global profile;
no single-instance/open-url routing). Rather than run them allow-failure (always
red, no signal), they are skipped: the macOS/Linux/Windows e2e matrices now run
only `standard` (single-window), and macOS `standard` stays the required gate.
The window/deeplink specs are kept (with NOT-RUN-IN-CI notes) for local runs
(TEST_TYPE=window|deeplink) and to re-fold into CI once electrobun ships per-window
partitions / per-instance root_cache_path / open-url routing. multiremote stays
maxInstances=1 (blocked upstream). See the agent-os plan "Framework gaps".
(Branch also drops the aligned --user-data-dir experiment — it cleared the profile
error but reintroduced CEF instance-folding → no CDP targets — restoring the
per-worker /tmp user-data-dir as the single-instance config.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): resolve the Linux/Windows bundle layout (unblock their e2e)
Linux/Windows e2e never launched — the config failed to load with "No Electrobun
.app bundle found": the conf globbed `**/*.app` (macOS-only) and the service's
non-macOS resolveElectrobunApp assumed build.json was a sibling of the binary.
Electrobun actually emits `build/<env>/<App>/bin/launcher[.exe]` with
`<App>/Resources/build.json` (verified from the CI build artifacts).
- conf resolveElectrobunAppPath: OS-aware — macOS globs `*.app`; Linux/Windows glob
`**/bin/launcher[.exe]` (helper exes are `bun Helper (…)`, never `launcher`).
- service resolveElectrobunApp: when the binary is in a `bin/` dir, map the bundle
root to its grandparent and read build.json from `<root>/Resources` (flat-layout
sibling fallback kept).
Lets the Linux/Windows `standard` legs start so we can see whether their CEF serves
CDP (Windows sets remote_debugging_port live; Linux has the CefSettings field
commented out but may still get the flag via chromiumFlags).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): install libwebkit2gtk on Linux + always open 2 windows (CDP targets)
Two fixes so the standard e2e launches + exposes CDP across OSes:
- Linux: electrobun's libNativeWrapper.so links libwebkit2gtk-4.1 (it supports both
the WebKit and CEF renderers), so it failed to dlopen on the CI runner
("libwebkit2gtk-4.1.so.0: cannot open shared object file") and the app never
started — Chromedriver then timed out creating the session. Add libwebkit2gtk-4.1-0
to the Linux runtime deps.
- macOS/Windows: revert the single-window change. Empirically a single CEF window
doesn't reliably expose a /json page target (the chrome-runtime global-context
fallback) → "No CDP page targets" (~1/6 specs pass); two windows expose the main
target (4–6/6). The fixture opens both windows for every suite again; the second
view also backs the CI-skipped window suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): wait for CEF /json before attach + next Linux runtime lib
- Launcher waits for CEF to serve /json with a page target (waitForCdpReady) before
the worker's Chromedriver attaches via debuggerAddress. Without it, Chromedriver
raced CEF's (slow, on Windows) port binding → "cannot connect to chrome at
localhost:N" → 120s session timeout × retries (the 11-min Windows run, 1/6 specs).
Resolves with a warning on timeout — a failed attach is the real signal.
- Linux: add libayatana-appindicator3-1, the next lib libNativeWrapper.so needs to
dlopen after libwebkit2gtk (incremental electrobun native dep chain).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): 127.0.0.1 CDP attach + skip darwin-path unit tests on Windows + format
- e2e: attach to CEF over 127.0.0.1, not 'localhost'. CEF binds the debugger on IPv4,
but Node/Chromedriver resolve 'localhost' to IPv6 ::1 first on Windows/Linux CI, so
both the /json readiness poll (waitForCdpReady) and the Chromedriver attach failed
there (30s timeouts, "cannot connect to chrome"). The bridge inherits the host via
parseDebuggerAddress, so it connects on IPv4 too. (macOS already worked.)
- unit: skip nativeMode's darwin-mocked clone/spawn/teardown suites on Windows — they
assert hardcoded POSIX paths node:path can't match on a Windows runner (logic is
OS-identical since the platform is mocked, covered on Linux/macOS; real Windows
behaviour is exercised by e2e). Fixes the Unit [Windows] job. Aliased to describe.skip
so vitest/valid-describe-callback accepts it.
- lint: reflow an over-long line in electrobunConfig.spec.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): run the CEF app under xvfb-run on Linux (headless CI display)
Linux CI runners are headless; CEF is a GUI process and failed with "Failed to open
X11 display" → no browser → no /json (the Linux e2e leg's real blocker, not the CDP
port). WDIO's autoXvfb covers the worker process but not this launcher-spawned app,
so spawn it under `xvfb-run -a` on Linux (macOS/Windows runners have a real display).
Add the xvfb package to the Linux CI deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): drop the /tmp --user-data-dir so the CEF profile creates (catch-22)
The cross-OS e2e blocker was "Cannot create profile at <root_cache_path>/partitions/
default": BrowserWindow forces a persist:default partition whose on-disk profile lives
under CEF's root_cache_path, but the service pinned a /tmp --user-data-dir — and the
chrome-runtime only creates profiles INSIDE --user-data-dir, so the partition was
orphaned → a racy global-context fallback (recoverable on macOS ≈5/6, fatal on
Linux/Windows). Stop injecting --user-data-dir: CEF then uses its own root_cache_path
as the user-data-dir, so the partition profile lands inside it and creates cleanly.
Trade-off: instances share root_cache_path, so this is single-instance (maxInstances=1);
multiremote stays blocked pending an upstream CEF fix (per the agent-os "Framework gaps").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(electrobun): stagger the 2nd window behind mainview's dom-ready (macOS render race)
macOS standard was flaky (4–6/6): whichever spec's app instance lost CEF's
global-context race failed with "fixture #app-title never rendered" (mainview's DOM
unpainted). Opening both CEF windows concurrently lets a browser spawn a separate
top-level window instead of embedding via SetAsChild. Open the second window only
after mainview's dom-ready, so mainview embeds + paints cleanly first; the second
view still opens (needed so the bridge can enumerate window-1). Targets stay
enumerable because both windows are up by the time the bridge attaches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(electrobun): macOS-only e2e — remove Linux/Windows build + e2e jobs
CEF can't serve /json on Linux/Windows: the chrome-runtime can't create the forced
persist:default partition profile, and unlike macOS its global-context fallback
doesn't recover there (no /json → the bridge never attaches) — an upstream electrobun
limitation. Running those legs only burned slow ~150MB CEF builds and produced red
noise, so remove the Linux/Windows build + e2e jobs entirely (rather than
allow-failure). v1 ships macOS-only (pre-1.0); re-add when electrobun makes the
failed-profile fallback ephemeral-per-webview. See the implementation plan
"Framework gaps".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(skills): add-native-service — pre-1.0 strategy when upstream blocks the surface
Add a "When upstream blocks the standard surface (shipping pre-1.0)" section: ship the
working subset rather than blocking on upstream, version it 0.1.0 (not 1.0.0-next.0)
with minor bumps as upstream lands fixes and 1.0.0 at full convergent-surface parity,
SKIP (don't allow-failure) the upstream-blocked CI legs, fail fast with a runtime
SevereServiceError on unsupported platforms, keep blocked specs (skipped + local-only),
and file upstream issues. Electrobun (macOS-only, 0.1.0) is the worked example. Also
note the 0.1.0 release-notes path in ci-and-release.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(electrobun): drop stale CFFIXED_USER_HOME from launcher class JSDoc
The launcher no longer redirects the CEF cache root (CFFIXED_USER_HOME / per-worker
--user-data-dir were both disproven, then removed): CEF uses its own root_cache_path,
which makes v1 single-instance macOS-only. Update the class JSDoc to match — drop the
CFFIXED_USER_HOME mention + the "parallel-safe" claim, and add the /json-readiness wait
step. See the implementation plan "Framework gaps".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(skills): add-native-service — refine upstream-gap workflow + local-checkout precursor
When upstream blocks the surface: aggregate gaps in the plan, search existing issues
(open + closed) first, and file ONE umbrella issue connecting the upstream's existing
issues to the consumer goal rather than N duplicates. Cover the edge cases the first
pass missed: net-new gaps (no existing issue) are captured inline in the umbrella and
split into their own issue only when independently actionable; pick the filing shape by
whether aggregation helps triage (count is a heuristic) — one gap never gets an umbrella,
two related gaps share one combined issue, three+ related gaps get the full umbrella.
Also make checking out the target framework's source locally an explicit, non-optional
Phase 0 precursor — it's read constantly to confirm the archetype and cite gap source
refs. Update the electrobun worked example with the real issue map (#380/#445/#448 +
closed #278/#122).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(electrobun): retry e2e spec files to absorb the residual macOS CEF render race
The macOS `standard` suite occasionally fails one spec with `#app-title never rendered`.
The 2-window fixture (needed because a single CEF window exposes no `/json` target) trips
CEF's failed-profile -> global-context fallback into spawning a separate top-level window
instead of embedding, leaving the main view unpainted for that app instance's whole life.
mochaOpts.retries can't escape it (same instance); specFileRetries re-spawns a fresh CEF
instance. Upstream race (plan "Framework gaps") — honest mitigation until the
ephemeral-per-webview fallback lands upstream.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(electrobun): fix stale spawnElectrobunApp JSDoc (no --user-data-dir pinned)
The JSDoc claimed spawnElectrobunApp pins a per-run --user-data-dir into the clone's
build.json, contradicting both the inline comment and the implementation, which
deliberately pins ONLY the port (CEF's own root_cache_path stays the user-data-dir so
the forced persist:default profile creates cleanly — the single-instance trade-off).
Align the JSDoc with the code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(electrobun): drop redundant post-loop spawnedAppsByCid.set
onWorkerStart already sets spawnedAppsByCid inside the loop (so a waitForCdpReady
failure still leaves the spawned app tracked for teardown), and workerApps is stored
by reference — the subsequent push()es are already visible through the map. The set
after the loop re-stored the same reference, a no-op. Removing it also avoids adding
a spurious empty entry when a worker has no capabilities.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| packages/electrobun-service/src/launcher.ts | Launcher correctly clones the bundle per-worker, pins the CDP port into build.json, and waits for the CEF /json endpoint before setting debuggerAddress. The spawnApp seam, APFS fast-path fallback, and per-cid cleanup are well handled. |
| packages/electrobun-service/src/nativeMode.ts | Process management is sound: clone cleanup on pin failure, SIGTERM→SIGKILL grace window, and log handler teardown. The deliberate omission of --user-data-dir is well-documented inline. waitForCdpReady always resolves (with a warning on timeout). |
| packages/electrobun-service/src/service.ts | Worker service attaches the CDP bridge and installs browser.electrobun.*. syncWebDriverWindow is best-effort but can leave the browser on the last-tried handle when no match is found — could confuse test debugging in multi-window scenarios. |
| packages/electrobun-cdp-bridge/src/connection.ts | WebSocket lifecycle handling is correct: CONNECT_PROMISE_ID at 0, command IDs starting at 1, per-send timeouts, and double-close safety. The async on the message handler is unnecessary since #messageHandler is synchronous, but harmless. |
| packages/electrobun-cdp-bridge/src/bridge.ts | Multi-target bridge is well-structured: per-field ?? defaults fix the previously-undefined-timeout bug, #discover retry loop is bounded, and active-target auto-advance on window close is correctly handled. |
| packages/electrobun-service/src/mock.ts | Mock implementation is solid: idempotent install, one-way sync of call history, and proper clear/reset/restore delegation. Error serialization for mockRejectedValue does not include stack trace unlike the read-back path. |
| packages/electrobun-service/src/innerRecorder.ts | In-page spy factory and script builders are well-implemented with valid-target guard, pathLiteral escaping, and circular-reference protection in the read-back serializer. |
| packages/electrobun-service/src/electrobunConfig.ts | Bundle resolution and port-pinning are well-guarded. The writeRemoteDebuggingPort docstring describes a user-data-dir use case that conflicts with the explicit design decision in nativeMode.ts to never inject one. |
| packages/electrobun-service/src/session.ts | Standalone session init/cleanup is correctly sequenced — launcher tracked before service.before, cleaned up on both service.before failure and remote session failure. |
| packages/electrobun-cdp-bridge/src/targetRegistry.ts | URL-sorted label assignment and stable reconcile logic are correct. labelOrder returns NaN for unexpected label formats but labels are always 'main' or 'window-N' so this is unreachable in practice. |
Sequence Diagram
sequenceDiagram
participant LCI as CI Runner
participant L as ElectrobunLaunchService (main)
participant NM as nativeMode (cloneBundle / spawn)
participant CEF as Electrobun App (CEF)
participant W as ElectrobunWorkerService (worker)
participant B as CdpBridge
participant T as Test
LCI->>L: onPrepare(caps)
L->>L: resolveElectrobunApp + verifyCefRenderer
L->>L: force browserName:'chrome'
LCI->>L: onWorkerStart(cid, caps)
L->>NM: spawnElectrobunApp(cloneBundle, pinPort)
NM->>NM: cp -Rc bundle to tmp clone
NM->>NM: writeRemoteDebuggingPort into build.json
NM->>CEF: spawn clonedBinary
NM-->>L: ElectrobunAppProcess
L->>CEF: waitForCdpReady (poll /json)
CEF-->>L: page target ready
L->>L: set goog:chromeOptions.debuggerAddress
LCI->>W: service.before(caps, browser)
W->>B: new CdpBridge(host, port)
B->>CEF: WebSocket connect
B->>CEF: Runtime.enable
W->>W: installApi + syncWebDriverWindow
T->>W: browser.electrobun.execute(fn)
W->>B: bridge.send(Runtime.evaluate)
B->>CEF: CDP evaluate
CEF-->>W: result
T->>W: browser.electrobun.mock(target)
W->>CEF: buildInstallScript via Runtime.evaluate
W-->>T: ElectrobunMock
LCI->>L: onWorkerEnd(cid)
L->>NM: stopElectrobunApp SIGTERM/SIGKILL
NM->>NM: rmSync cloneParentDir
LCI->>W: service.after()
W->>B: bridge.close()
Reviews (1): Last reviewed commit: "feat(electrobun): E2E suite + CI build/t..." | Re-trigger Greptile
- mock.ts: include `stack` when serialising an Error mock value (mockRejectedValue), matching the read-call-data path — no asymmetry for users inspecting thrown errors. - electrobunConfig.ts: fix writeRemoteDebuggingPort JSDoc — the userDataDir param is supported but the launcher intentionally never passes it (the disproven approach), contradicting the old "isolates each worker's profile" claim. - service.ts: syncWebDriverWindow restores the caller's original window handle on the no-match path instead of stranding the session on the last-probed handle. - connection.ts: drop the unnecessary `async` on the ws 'message' handler (the body is sync; only the catch is async) — `void this.#errorHandler(error)`. - service.spec.ts: add getWindowHandle to the browser mock for the restore path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…CI, release pipeline (PR5) (#318) * docs(skills): correct pre-1.0 version guidance to the 0.1.0-next.0 placeholder The "start at 0.1.0" wording conflated the in-repo dev placeholder with the published version. The repo convention is a `X.Y.0-next.0` placeholder that releases as stable `X.Y.0` on `latest` (with `-next.N` prereleases on `next`) — so the pre-1.0 form is a `0.1.0-next.0` placeholder releasing as `0.1.0`, NOT a bare `0.1.0`. Clarify both the "When upstream blocks" version section and the Phase 1 conventions line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(electrobun): set 0.1.0-next.0 dev version for both packages The service ships pre-1.0 (0.x) because upstream blocks Linux/Windows/multiremote/ multi-window/deeplink — 1.0 is reserved for full parity once those gaps fill. Relabel the dev placeholder from 1.0.0-next.0 (which implied a 1.0 target) to 0.1.0-next.0; it releases as stable 0.1.0 on `latest`, with 0.1.0-next.N prereleases on `next`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(electrobun): fail fast in native mode on non-macOS (CEF macOS-only in 0.x) CEF-rendered Electrobun apps are only drivable on macOS in the 0.x line: on Linux/Windows CEF's failed-persist:default-profile fallback serves no /json, so Chromedriver can never attach (upstream-blocked). Throw a clear SevereServiceError in launcher.onPrepare's native branch instead of letting users hit a cryptic CDP-attach timeout — the message points to macOS / browser mode and the native-renderer follow-up (#317). Browser mode is unaffected. Tests stub process.platform (darwin happy-path + linux/win32 guard) per the dioxus pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): move browser-mode guard test into the browser-mode describe Greptile P2 (#318): the "native-mode macOS guard does not fire in browser mode" case exercises browser mode but sat in the onPrepare native-mode describe. Move it beside the analogous mixed-mode test in the browser-mode describe so each block is self-contained. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(scripts): support electrobun in test-package + rename service both→all Thread the electrobun service through scripts/test-package.ts (CDP archetype like electron: service + native-types + electrobun-cdp-bridge; a macOS-only skipBuild path that copies the pre-built CEF `build/` bundle rather than rebuilding). Electrobun reuses the existing cdpBridgePath field — it never co-packs with electron (not part of the aggregate), so the field is unambiguous per run. Also rename the stale `both` SERVICE option → `all` (it was a two-service artifact; now means electron+tauri+dioxus — electrobun is excluded, it's macOS-only and always run via its own `--service=electrobun` job). The moduleType `both` (cjs+esm) is unrelated and unchanged. Flatten the now-three-way service-detection into a prefix lookup table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci(electrobun): wire the macOS-only package-test (build + package jobs) Add the package-test CI plumbing for @wdio/electrobun-service (macOS-only, CEF): - _ci-build-electrobun-package-app.reusable.yml — builds the electrobun-app fixture (Bun/CEF) and uploads a symlink-preserving tar (like the e2e build, not the zip upload-archive the dioxus/tauri package builds use). - _ci-package.reusable.yml — `electrobun` arm (download + untar the bundle, pack the service + cdp-bridge, run test:package:electrobun --skip-build); also rename the service input `both` → `all` to match scripts/test-package.ts. - ci.yml — build-electrobun-package-app-macos-arm + package-electrobun-macos-arm jobs, both gated on run_electrobun and added to ci-status needs. - _ci-detect-changes — add the new build workflow to infra_electrobun. - package.json — test:package:electrobun script. Fixture test harness (wdio.conf + smoke spec + `test` script) lands in the next commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): add the package-install smoke harness to the electrobun-app fixture PR3 stubbed the fixture with only a `build` script. Add the wdio test harness so the package-test can actually run: - src/bun/index.ts: open TWO staggered CEF windows (a single window exposes no /json target after CEF's persist:default→global-context fallback — same workaround the e2e fixture uses). - wdio.conf.ts: macOS-only .app resolution, @wdio/electrobun-service native mode, browserVersion pinned to CEF's Chromium 147, specFileRetries for the residual race. - test/smoke.spec.ts: install smoke — launch, attach over CDP, read #app-title + #status. - tsconfig.wdio.json (+ @types/mocha) and a `test` script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(electrobun): address Greptile review on PR #314 (4 × P2) - mock.ts: include `stack` when serialising an Error mock value (mockRejectedValue), matching the read-call-data path — no asymmetry for users inspecting thrown errors. - electrobunConfig.ts: fix writeRemoteDebuggingPort JSDoc — the userDataDir param is supported but the launcher intentionally never passes it (the disproven approach), contradicting the old "isolates each worker's profile" claim. - service.ts: syncWebDriverWindow restores the caller's original window handle on the no-match path instead of stranding the session on the last-probed handle. - connection.ts: drop the unnecessary `async` on the ws 'message' handler (the body is sync; only the catch is async) — `void this.#errorHandler(error)`. - service.spec.ts: add getWindowHandle to the browser mock for the restore path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scripts): exclude electrobun-* from the `all` service fixture scan `all` (the no-arg default of `pnpm test:package`) scanned every fixture, including electrobun-app — but buildAndPackService('all') never packs the electrobun tarball, so testExample threw "Electrobun service packages not available", crashing a bare local run. CI is unaffected (it always passes an explicit --service), but the local DX regression was real. Exclude electrobun-* from `all` to match the documented intent ("never part of all"); run it via `--service=electrobun`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): assert the script-set #status text in the package smoke The package-test run showed the harness works (CEF build + launch + CDP attach + execute all green); only the #status assertion was wrong — the mainview script overwrites #status to "Application loaded successfully" on load, not the static "Ready for testing". Assert the script-set text (which also confirms the view JS ran) and poll for it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci(electrobun): add electrobun release scope to the pipeline - release.yml: add `electrobun` to the workflow_dispatch scope choices. - _release.reusable.yml: target set `@wdio/electrobun-service,@wdio/electrobun-cdp-bridge` + a build case (CDP, no Rust/build:rust — mirrors electron). No GTK/Rust toolchain steps. - releasekit.config.json: skip the private electrobun fixtures (electrobun-app-example, electrobun-e2e-app) from versioning/publish; map scope:electrobun → @wdio/electrobun-*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(electrobun): document the 0.1.0 macOS-only ship + known limitations - electrobun-service/README.md: rewrite from the stale PR1 foundation copy — 0.1.0, macOS-only, CEF-renderer requirement, quick start, the supported surface, and the upstream-blocked known limitations (Linux/Windows, multiremote, multi-window, deeplink) with the #317 link. - README.md: add the @wdio/electrobun-service entry + package-tree + framework line. - AGENTS.md: add Electrobun to supported frameworks + the package tree. - ROADMAP.md: mark Phase 5 shipped (0.1.0, macOS-only) with the upstream caveat + #317. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(release): add dioxus to releasekit skip + scopeLabels (pre-existing gap) Surfaced while wiring electrobun: dioxus was missing from both releasekit.config.json sections that every other framework has. Latent because @wdio/dioxus-service is unpublished, but a future dioxus release would otherwise try to publish its private fixtures (electron/tauri fixtures are private yet still explicitly skipped) and scope:dioxus would resolve to no packages. Add dioxus-app-example + wdio-dioxus-e2e-app to version.skip and scope:dioxus → @wdio/dioxus-*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(electrobun): mark experimental in README + list across contributor docs - README.md: move electrobun into a dedicated "Experimental Support" section (0.x, macOS-only, CEF-required, #317) so it's clearly distinct from the 1.0 frameworks. - architecture.md / package-structure.md: add the service + cdp-bridge to the package tables + the browser.electrobun.* API-injection list. - e2e-testing.md: add the electrobun test dir + wdio.electrobun.conf.ts (macOS-only). - CONTRIBUTING.md: add the Electrobun row to the release-packages table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): hoist the Doc type to module scope in the package smoke Greptile P2 (#318): the `Doc` helper type was declared identically in two test callbacks. Hoist it to module scope (DRY + easier to extend). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): use idiomatic browserName 'electrobun' in the wdio configs Greptile P2 (#318): the package-test conf set browserName: 'chrome' directly, bypassing the launcher's electrobun→chrome rewrite and diverging from the sibling convention (browserName: 'tauri'/'dioxus'). Use 'electrobun' across the package-test conf, the e2e conf, and the README quick-start — the launcher coerces it to 'chrome' in onPrepare (already covered by launcher.spec), so it's a functional no-op that exercises the documented rewrite and makes the configs honest copy-paste templates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(electrobun): bump e2e specFileRetries 2→3 for the upstream CEF race The macOS `standard` gate failed twice running with an elevated rate of CEF's "Timeout of new browser info response" (the 2-window global-context race) — at specFileRetries:2 (3 attempts) one spec exhausted its retries. Bump to 3 (4 attempts/spec) to absorb the elevated rate. This is the documented upstream flake (#317), not a service regression — the browserName change is exonerated (same CEF timeout; package-test passes). Drop back once the upstream fix lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…/20 floor) Both packages advertised "^18.12.0 || ^20.9.0 || >=22.11.0" but the fixture/template wdio config uses fs.globSync (Node 22+), and the repo runs Node 24. Pre-1.0 + no consumers yet, so drop the legacy 18/20 floor and match the modern CDP sibling (@wdio/electron-service is >=22.12.0). Keeps the advertised engine consistent with the globSync usage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…not hand-authored The "Per-package release notes" section told you to hand-author docs/release-notes/<version>.md before the Ship PR. ReleaseKit actually generates the release notes + CHANGELOG at release time (configured under `notes` in releasekit.config.json); the committed docs/release-notes/*.md are generated artifacts. Don't hand-author them — put the supported-subset/limitations prose in the README/docs instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aggregate / integration PR for the new
@wdio/electrobun-service(ROADMAP Phase 5) — the combined 5-PR stack onfeat/electrobun-service, held for final review before merging tomain.Electrobun is a CDP-attach service (like Electron): the launcher spawns the CEF-rendered app and the worker drives it over the Chrome DevTools Protocol via Chromedriver
debuggerAddress, with@wdio/electrobun-cdp-bridgeas a multi-target side-channel. The Phase 0 spike validated the approach on macOS (agent-os/specs/20260528-electrobun-service/RESEARCH_FINDINGS.md).Status
All five phases are merged into this feature branch (the stack is complete). A couple of small post-merge tweaks landed directly on the branch (Node engines →
>=22.12.0, a skill docs correction). CI is being verified on the feature branch; this PR is held for final review — it is not auto-merged tomain.0.1.0), macOS-onlyDriving the E2Es on CI (PR4) surfaced a hard upstream CEF constraint:
BrowserWindowforces every window onto apersist:defaultpartition, which CEF's chrome-runtime can't create (root_cache_pathis hard-fixed, no override), so CEF falls back to a global browser context. macOS recovers (still serves/json→ Chromedriver attaches); Linux/Windows do not (the fallback serves no/json; Linux additionally hasremote_debugging_portcommented out). This lives in electrobun's prebuilt native lib — not fixable from the service.So the service ships
0.1.0(pre-1.0), macOS-only.0.xbecause upstream blocks a large part of the convergent surface;1.0is reserved for full parity once the gaps fill (minor bumps as each lands).Phased stack
What ships in
0.1.0(macOS)execute, mocking (mock+clear/reset/restoreAllMocks+isMockFunction), frontend + backend log capture, browser mode, standalone/session mode, headless — all validated by unit tests, package-install smoke, 3-OS unit/build-tooling, and the macOS single-window E2E (standardsuite). A runtimeSevereServiceErrorfails fast on Linux/Windows native mode (framed around the CEF renderer; browser mode still works cross-platform).Known limitations (upstream-blocked — the "Framework gaps")
/jsonthere (failed-profile fallback; Linux port commented out)root_cache_pathswitchWindow/listWindows(multi-window)triggerDeeplink(macOS)emitEventmockAll/ class-mockThe blocked/unreliable specs are retained with NOT-RUN-IN-CI notes (runnable locally via
TEST_TYPE=…). The macOSstandarde2e gate is intermittently flaky from the same upstream CEF race ("Timeout of new browser info"/ unpainted#app-title); mitigated with a staggered 2-window fixture +specFileRetries: 3. Occasional re-runs expected until the upstream fix lands.Follow-ups
blackboardsh/electrobun, linking the upstream's existing issues to the automation goal: profile isolation macOS CEF persistent partitions using custom request-context cache paths for cookies blackboardsh/electrobun#380 / CEF persistent partitions fail on macOS because profile path is nested under Partitions/ blackboardsh/electrobun#278 (closed) / CEF not work under linux blackboardsh/electrobun#448; remote-debugging opt-in Make CEF remote debugging opt-in blackboardsh/electrobun#445; prior E2E request Feature Request: E2E testing blackboardsh/electrobun#122 (closed); multi-window Tabs render blank after switching and crash on close in Multitab template (Wayland/CEF) | GLXBadWindow / Signal 11 blackboardsh/electrobun#281 / Multi-tab browser: only first URL load works, subsequent navigation and new tabs unresponsive blackboardsh/electrobun#253 / Feature request: Event or callback when <electrobun-webview> tag creates a BrowserView (for multi-tab RPC) blackboardsh/electrobun#155. (Fully-qualified cross-repo refs — these link to the electrobun repo, not this one.)Notes for reviewers
🤖 Generated with Claude Code