Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
6f141f1
feat(electrobun): PR1 foundation — package skeletons + native-types w…
goosewobbler May 30, 2026
d57dc71
docs(electrobun): Phase 0 spike findings — CDP-attach validated on macOS
goosewobbler May 30, 2026
14f4399
fix(electrobun): address Greptile review on PR #310
goosewobbler May 30, 2026
5292049
docs(electrobun): clarify remoteDebuggingPort is pinned per worker, n…
goosewobbler May 31, 2026
2f2382f
docs(electrobun): PR2 launch-time control investigation
goosewobbler May 30, 2026
e244b07
feat(electrobun-cdp-bridge): multi-target CDP connection manager
goosewobbler May 30, 2026
370442f
docs(electrobun): correct port mechanism — multiremote achievable via…
goosewobbler May 30, 2026
4b5d915
feat(electrobun): add electrobunConfig app-resolution + build.json he…
goosewobbler May 30, 2026
08ad9aa
feat(electrobun): implement native-mode launcher (single-instance)
goosewobbler May 30, 2026
6b8867a
feat(electrobun): install browser.electrobun surface + standalone ses…
goosewobbler May 30, 2026
7fae8df
docs(electrobun): record upstream multi-instance issue draft (not filed)
goosewobbler May 30, 2026
1235a56
fix(electrobun): address Greptile review on PR #311
goosewobbler May 31, 2026
15e0c37
fix(electrobun-cdp-bridge): clear/advance active target when pruned o…
goosewobbler May 31, 2026
3b8e3fe
fix(electrobun-cdp-bridge): clear send timeout on response + dedupe c…
goosewobbler May 31, 2026
e769f9a
feat(electrobun): implement triggerDeeplink (macOS)
goosewobbler May 31, 2026
4d48978
test(electrobun): add e2e + package-test fixture apps
goosewobbler May 31, 2026
9dd6be3
chore(fixtures): register electrobun fixtures in pnpm workspace
goosewobbler May 31, 2026
575aacb
feat(electrobun): clone bundle per worker for parallel native mode
goosewobbler May 31, 2026
fc5f650
feat(electrobun): implement browser.electrobun.mock + mock lifecycle …
goosewobbler May 31, 2026
17ed4e4
fix(electrobun): address Greptile review on PR #312
goosewobbler May 31, 2026
bc388ea
fix(electrobun): reject mixed browser/native capability sets in onPre…
goosewobbler May 31, 2026
044fd92
fix(electrobun): preserve Error.name in mock serialisation + clean cl…
goosewobbler May 31, 2026
08a4c3e
fix(electrobun): validate mock target as a dotted identifier path
goosewobbler May 31, 2026
f615045
fix(electrobun): complete JS-source escaping of mock target (clears C…
goosewobbler Jun 1, 2026
0ef0821
feat(electrobun): E2E suite + CI build/test wiring (PR4) (#316)
goosewobbler Jun 2, 2026
9896d8e
feat(electrobun): Ship — 0.1.0, macOS-only guard, docs, package-test …
goosewobbler Jun 2, 2026
4d4d262
chore(electrobun): require Node >=22.12.0 (drop the pre-1.0 legacy 18…
goosewobbler Jun 2, 2026
1a13736
docs(skills): correct release-notes guidance — ReleaseKit-generated, …
goosewobbler Jun 2, 2026
bc85af1
fix(electrobun): namespace-aware prefix match in allMocks (Greptile P2)
goosewobbler Jun 2, 2026
3d5a86c
fix(electrobun): best-effort bulk mock ops + clean cpSync fallback (G…
goosewobbler Jun 2, 2026
11c7a68
fix(electrobun): restore Error stack in page-side reconstruction (Gre…
goosewobbler Jun 2, 2026
c2d958b
fix(electrobun-cdp-bridge): nullish defaults, deterministic refresh +…
goosewobbler Jun 2, 2026
e036d7a
docs(electrobun): review polish — status parentheticals, bridge descr…
goosewobbler Jun 3, 2026
e0511f6
chore(electrobun): review polish — comment drift fixes, drop no-op in…
goosewobbler Jun 3, 2026
605ee95
fix(electrobun-cdp-bridge): drop async/return-await from ws error han…
goosewobbler Jun 3, 2026
caa4f93
fix(electrobun): address Greptile follow-ons — session teardown, Weak…
goosewobbler Jun 3, 2026
0468ac2
fix(electrobun-cdp-bridge): drop redundant socket-level timeout in de…
goosewobbler Jun 3, 2026
84ede1e
fix(electrobun): Greptile round — close orphaned connection, native-f…
goosewobbler Jun 3, 2026
6aee71b
fix(electrobun): Greptile round — nullish CDP params, mock-context er…
goosewobbler Jun 3, 2026
dad4afc
chore(electrobun): Greptile round — drop dangling env-var export, ann…
goosewobbler Jun 3, 2026
7038a9b
fix(electrobun): merge globalOptions into the standalone worker servi…
goosewobbler Jun 3, 2026
45f6614
fix(electrobun-cdp-bridge): codepoint sort in target registry (Greptile)
goosewobbler Jun 3, 2026
4627abe
fix(electrobun): parse-check mock implementation source (Greptile)
goosewobbler Jun 3, 2026
f53d18b
fix(electrobun): align worker browser-mode detection with the launche…
goosewobbler Jun 3, 2026
663427e
fix(electrobun-cdp-bridge): connect the auto-advanced target on refre…
goosewobbler Jun 4, 2026
9af8e68
fix(electrobun-cdp-bridge): first close reason wins in concurrent clo…
goosewobbler Jun 4, 2026
faec8fb
Merge remote-tracking branch 'origin/main' into feat/electrobun-service
goosewobbler Jun 4, 2026
3c7d167
chore(electrobun): neutral wording for the cp -Rc fallback log (Grept…
goosewobbler Jun 4, 2026
3214579
fix(electrobun): teardown hardening — post-SIGKILL reap-wait, best-ef…
goosewobbler Jun 4, 2026
2682a1f
fix(electrobun): Greptile round — maxInstances warning, closed-bridge…
goosewobbler Jun 4, 2026
f4ca26a
fix(electrobun): escape U+2028/29 in all inlined JSON, guard lazy con…
goosewobbler Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 93 additions & 6 deletions .claude/skills/add-native-service/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,104 @@ New services build **on `@wdio/native-core`** (Tauri and Dioxus do; Electron pre

→ **Full inventory, the known divergences to converge (Electron's window model, Dioxus's missing `emitEvent`), and the pattern behind each:** [features.md](features.md).

## When upstream blocks the standard surface (shipping pre-1.0)

Some frameworks — especially a **beta/pre-1.0 upstream** — can't yet support the full convergent
surface above: a platform may have no working automation path, or a standard feature (multiremote,
multi-window, deeplink) may be blocked by a framework/runtime limitation you **cannot fix from the
service layer**. Don't force-fit, and don't hold the whole package hostage to upstream — ship the
working subset, clearly scoped, and recover the rest as upstream lands fixes.

**Version: base at `0.1.0`, not `1.0.0`.** The default convention here is a `X.Y.0-next.0` dev
placeholder in `package.json` that releases as stable `X.Y.0` on `latest` (with `-next.N` prereleases
on the `next` dist-tag in between — every service does this: electron `10.0.0-next.N`→`10.0.0`, tauri
`1.0.0-next.N`→`1.0.0`). A full-convergent-surface service bases that at `1.0` (placeholder
`1.0.0-next.0`, release `1.0.0`). When upstream blocks a lot, `1.0` over-promises — base it at **`0.x`**,
the semver signal for "early, partial, scope may change, gaps expected". Keep the same release
*machinery*, just lower the base:
- **Dev placeholder `0.1.0-next.0`** (a prerelease *of* 0.1.0 — NOT `1.0.0-next.0`, which implies a 1.0 target).
- **First stable release `0.1.0`** on `latest`; `0.1.0-next.N` prereleases on `next` during lead-up. (`0.x` = unstable API; `-next.N` = staging channel — orthogonal, you want both.)
- **Minor bumps** (`0.2.0`, `0.3.0`…) as each upstream fix recovers a platform/feature. Breaking changes are allowed within `0.x` (bump minor).
- **Graduate to `1.0.0`** only at full parity with the sibling services — the whole standard surface on all intended platforms. `1.0` is the promise that the convergent surface works.

**Be honest in CI — skip, don't allow-failure.** For a platform/suite that's **blocked upstream**
(not merely flaky), **remove its jobs entirely** rather than marking them allow-failure. Allow-failure
legs that can *never* pass only burn CI minutes (slow runtime downloads, etc.) and add permanent red
noise that trains everyone to ignore the column. Keep the validated platform/suite as the **required
gate**; leave a comment on the removed jobs naming the upstream blocker and the condition to re-add
them. (Reserve allow-failure for legs that are *unverified-but-plausible*, not *known-blocked*.)

**Fail fast at runtime.** Add an explicit `SevereServiceError` in `launcher.onPrepare` for an
unsupported platform/mode, with an actionable message ("`<service>` is macOS-only in v1 — `<platform>`
is blocked by `<upstream issue>`"). A clear early throw beats letting users hit a cryptic
attach/connection timeout. Gate it on a platform parameter (not bare `process.platform`) so the
launcher tests can exercise both branches.

**Keep the blocked specs, don't delete them.** Leave the blocked-feature e2e specs in the tree with a
NOT-RUN-IN-CI header comment, runnable locally via `TEST_TYPE=…`, and excluded from the CI matrix.
They are the re-enable checklist for when upstream lands.

**Document + file upstream — aggregate in the plan first, then ONE issue.** As you discover gaps,
collect them into a dedicated **"Upstream fixes needed"** section of the implementation plan — each
with its impact on the surface, the feature/platform it unblocks, and **exact source refs**
(`file:line`). Gaps surface across many debugging sessions; without one home in the plan they get lost,
and that section becomes the turnkey brief for the post-ship filing step. Before filing anything,
**search the upstream repo's issues (open *and* closed)** for every gap: a young/beta upstream usually
already tracks several, and a **closed** issue often explains current behaviour — e.g. a "completed"
fix that was really a band-aid fallback is frequently *why* one platform works while others don't.
Then choose the filing shape by **whether aggregation actually helps triage** — the gap *count* is
only a heuristic, so don't reflexively build (or skip) an umbrella on a number alone:
- **One gap → never an umbrella.** Nothing to aggregate. If it's already tracked upstream, comment
on / +1 the existing issue; if it's net-new, file one focused issue.
- **Two gaps → usually still no umbrella, but combine if related.** If the two share a root cause or
the same consumer goal, file **one issue covering both** — a lightweight combined issue, *not* the
full see-also structure. If they're unrelated, handle each on its own (comment on the existing
issue, or file a focused one). The deciding question is "does framing them together help the
maintainer?", not "are there two of them?".
- **Three or more related gaps → ONE umbrella issue**, framed around the consumer goal ("drive `<framework>`
apps with external WebDriver/CDP automation"). For each gap that **already has an issue**, link it
(`see also #N`) — don't duplicate. Each **net-new** gap (no existing issue) is captured *by the
umbrella itself*, since the umbrella is a new issue: describe it inline as its own section with
source refs. Only **split a net-new gap into its own dedicated issue** (then link it from the
umbrella, `see also #N`) when it's large and cleanly separable enough that the maintainer would want
to triage/close it independently — otherwise inline is enough. A single well-researched issue
connecting the maintainer's own scattered issues to a concrete use case triages far better — and
gives you **one canonical URL** to link everywhere — than several parallel issues that duplicate
what's already filed. Drop a one-line cross-link comment on the most directly related existing issues.

Record every gap as a known limitation in the service README/docs +
`ROADMAP.md`, and link the umbrella issue into the docs + the CI re-add notes. As each underlying fix
lands: re-add the job, drop the runtime-guard branch, lift the docs limitation, and bump the minor
version.

> **Worked example — `@wdio/electrobun-service`.** Electrobun's CEF chrome-runtime can't create the
> `persist:default` partition profile its `BrowserWindow` forces; macOS recovers via a global-context
> fallback but Linux/Windows serve no `/json`, and multiremote/multi-window/deeplink all trace to the
> same gap — none fixable from the service. So v1 ships **macOS-only, single-window, `0.1.0`**: the
> Linux/Windows build+e2e jobs are removed (not allow-failure), the window/deeplink specs are
> skipped-but-kept, and a macOS-only runtime guard fails fast elsewhere. The plan's "Framework gaps"
> aggregates every gap with source refs; the **search-first** pass found the upstream already tracking
> most of them — `#380` (the proper profile-isolation fix), `#445` (remote-debugging opt-in, but only
> noting macOS — Linux's `remote_debugging_port` is *commented out*), `#448` (a user hitting the same
> Linux profile error), plus `#278`/`#122` **closed** (the global-context band-aid that explains macOS
> recovery, and a prior e2e request). So rather than file four duplicates, the post-ship step is **one
> umbrella issue** ("enable external WebDriver/CDP automation for CEF apps") that links those, adds the
> net-new findings (the Linux commented-out port; the macOS-recovers/others-don't `/json` asymmetry;
> single-instance lock + `open-url` routing for deeplink — which has *no* existing issue), and is the
> one URL linked from the docs.

## Process

### Phase 0 — Pre-implementation spike

Validate platform constraints before any production code. Spikes are throwaway — they live in `/spike/<service>-spike/` (gitignored); the output is the **findings doc**, not the source.

1. Create a minimal app exercising the framework's public API.
2. **Confirm the Step 0 archetype** (does it expose CDP? which driver model? plugin or bridge?).
3. Identify the single most consequential unknown and write code that exercises it. Examples: "can a third-party crate flip Wry's `set_allows_automation`?" (Dioxus); "what's the CDP debugger-port discovery convention for this runtime?" (a CDP framework); "does the framework expose multiple addressable webview targets?".
4. Write `spike/FINDINGS.md`: the question, the answer with citations, the decision tree, and any **platform-by-platform variance** (often the most important section — e.g. the Dioxus Wry API is a no-op on Win/Mac but blocking on Linux).
5. Fold findings into the implementation plan: Risks, Platform Matrix, Phasing.
1. **Check out the target framework's source locally — a hard precursor, not optional.** Clone the upstream repo at a known-good *released* tag (don't `file:`-link it into the build; pin the published release — see Risks). You will read it constantly throughout the whole project: to confirm the Step 0 archetype, to find the runtime's debug-port / automation conventions, and — every time you later hit an upstream gap — to trace it to an exact `file:line` you can cite. A service built without the framework source open beside you is guesswork; note its path in the plan (e.g. `~/Workspace/<framework>`) so later sessions reuse it.
2. Create a minimal app exercising the framework's public API.
3. **Confirm the Step 0 archetype** (does it expose CDP? which driver model? plugin or bridge?) — by reading the source from step 1, not by assuming.
4. Identify the single most consequential unknown and write code that exercises it. Examples: "can a third-party crate flip Wry's `set_allows_automation`?" (Dioxus); "what's the CDP debugger-port discovery convention for this runtime?" (a CDP framework); "does the framework expose multiple addressable webview targets?".
5. Write `spike/FINDINGS.md`: the question, the answer with citations (`file:line` into the local checkout), the decision tree, and any **platform-by-platform variance** (often the most important section — e.g. the Dioxus Wry API is a no-op on Win/Mac but blocking on Linux).
6. Fold findings into the implementation plan: Risks, Platform Matrix, Phasing.

### Phase 1 — TypeScript service skeleton (shared, all archetypes)

Expand All @@ -113,7 +200,7 @@ src/

**Conventions:**

- Initial npm version `1.0.0-next.0`. Build script: `tsx ../../scripts/build-package.ts`.
- Initial `package.json` dev placeholder `1.0.0-next.0` for a service that reaches the full convergent surface on its target platforms (releases as stable `1.0.0`) — or **`0.1.0-next.0`** (releases as `0.1.0`) if upstream blocks a lot of the surface (see [When upstream blocks the standard surface](#when-upstream-blocks-the-standard-surface-shipping-pre-10)). Build script: `tsx ../../scripts/build-package.ts`.
- Mirror the closest sibling's `package.json` exactly (exports, scripts, devDeps, peerDeps): CDP → clone `@wdio/electron-service`; Wry → clone `@wdio/tauri-service`. Always depend on `@wdio/native-core`, `@wdio/native-spy`, `@wdio/native-types`, `@wdio/native-utils` as workspace deps.
- `vitest.integration.config.ts` MUST set `fileParallelism: false` + 30s timeout + `setupFiles: ['test/integration/setup.ts']`.
- `tsconfig.json` extends `../../tsconfig.base.json`, out `./dist`, root `./src`.
Expand Down
12 changes: 9 additions & 3 deletions .claude/skills/add-native-service/ci-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ To add a framework:
- **Rust setup + GTK libs**: the "Setup Rust" and "Install GTK development libraries" steps run `if: contains(inputs.scope, 'tauri') || contains(inputs.scope, 'dioxus')`. Add `|| contains(inputs.scope, '<framework>')` for a new Wry framework. CDP frameworks need neither.
3. Rust crates publish to crates.io via `CARGO_REGISTRY_TOKEN` (from `crates_io_token` secret); npm packages publish with provenance. Both already wired in the `Run ReleaseKit` step — listing the crate in the target set is enough.

### Per-package release notes

Each shipped service keeps `docs/release-notes/v1.0.0.md`; Wry crates keep `docs/release-notes/` too (e.g. `dioxus-bridge/docs/release-notes/v1.0.0.md`). Add the v1 note before the Ship PR.
### Release notes — generated by ReleaseKit, do NOT hand-author

ReleaseKit generates the release notes + `CHANGELOG.md` as part of the release (configured under
`notes` in `releasekit.config.json`; LLM-enhanced when an Ollama key is present). The
`docs/release-notes/<version>.md` files in the repo (e.g. `dioxus-service/docs/release-notes/v1.0.0.md`)
are **generated artifacts committed by a release**, not pre-Ship inputs — so do **not** hand-author a
`docs/release-notes/<version>.md` for a new service; it appears when the release runs. Listing the
package in the `_release.reusable.yml` target set is enough. Supported-subset/limitations prose is
hand-authored in the service README + `docs/` instead.
137 changes: 137 additions & 0 deletions .github/workflows/_ci-build-electrobun-e2e-app.reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Build Electrobun E2E App
# Description: Builds the Electrobun E2E test application (CEF bundle) and creates shareable artifacts.
#
# Electrobun has NO Rust component (unlike Dioxus) — it builds with Bun and bundles
# CEF. The CEF toolchain is a beta/unverified path: `electrobun build` downloads a
# ~150MB CEF framework, so the build is slower and more network-dependent than the
# other e2e-app builds. macOS is the only validated platform; Windows/Linux bundle
# layout is unverified (see packages/electrobun-service RESEARCH_FINDINGS).

permissions:
contents: read

on:
workflow_call:
inputs:
os:
description: 'OS of runner'
required: true
type: string
build_id:
description: 'Build ID from the main build job'
type: string
required: false
artifact_size:
description: 'Size of the build artifact in bytes'
type: string
required: false
cache_key:
description: 'Cache key to use for downloading artifacts'
type: string
required: false
outputs:
build_id:
description: 'Unique identifier for this build'
value: ${{ jobs.build-electrobun-e2e-app.outputs.build_id }}
build_date:
description: 'Timestamp when the build completed'
value: ${{ jobs.build-electrobun-e2e-app.outputs.build_date }}

env:
TURBO_TELEMETRY_DISABLED: 1
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
build-electrobun-e2e-app:
name: Build
runs-on: ${{ inputs.os }}
outputs:
build_id: ${{ steps.build-info.outputs.build_id }}
build_date: ${{ steps.build-info.outputs.build_date }}
steps:
- name: 👷 Checkout Repository
uses: actions/checkout@v6
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}

- name: 🛠️ Setup Development Environment
uses: ./.github/workflows/actions/setup-workspace
with:
node-version: '24'

- name: 🥟 Setup Bun
uses: oven-sh/setup-bun@v2
with:
# Pinned for reproducibility — the beta CEF toolchain is fragile, so a
# silent Bun bump must not break `electrobun build`. Bump deliberately.
bun-version: '1.3.14'

- name: 📊 Generate Build Information
id: build-info
shell: bash
run: |
echo "build_id=$(date +%s)-${{ github.run_id }}-${{ runner.os }}" >> "$GITHUB_OUTPUT"
echo "build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT"

- name: 📦 Download Package Build Artifacts
uses: ./.github/workflows/actions/download-archive
with:
name: wdio-desktop-mobile
path: wdio-desktop-mobile-build
filename: artifact.zip
cache_key_prefix: wdio-desktop-build
exact_cache_key: ${{ inputs.cache_key || github.run_id && format('{0}-{1}-{2}-{3}{4}', 'Linux', 'wdio-desktop-build', 'wdio-desktop-mobile', github.run_id, github.run_attempt > 1 && format('-rerun{0}', github.run_attempt) || '') || '' }}

- name: 📊 Show Build Information
if: inputs.build_id != '' && inputs.artifact_size != ''
shell: bash
run: |
echo "::notice::Build artifact: ID=${{ inputs.build_id }}, Size=${{ inputs.artifact_size }} bytes"

- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile
shell: bash

# CEF download + bundle. The beta Bun/CEF toolchain is the biggest unknown in
# this pipeline (~150MB CEF download); a failure here is most likely a
# toolchain/network issue rather than a regression in the fixture itself.
- name: 🏗️ Build Electrobun E2E App
run: pnpm run build
shell: bash
working-directory: fixtures/e2e-apps/electrobun

- name: 🔎 Locate Built Bundle
id: locate
shell: bash
run: |
# shellcheck disable=SC2012
echo "Build output tree:"
ls -R fixtures/e2e-apps/electrobun/build || true
if [ ! -d "fixtures/e2e-apps/electrobun/build" ]; then
echo "::error::Electrobun build directory was not produced."
exit 1
fi

# NOT the shared zip-based upload-archive action: the macOS .app carries CEF
# framework symlinks that `zip -r` resolves/duplicates, and download-archive's
# extract heuristic only restores dist/target dirs (never build/). A
# symlink-preserving tarball shared via actions/upload-artifact round-trips the
# bundle intact.
- name: 🗜️ Archive bundle (tar preserves .app framework symlinks)
shell: bash
run: |
# On Windows, $RUNNER_TEMP is a drive path (D:\a\_temp) and GNU tar reads the
# `D:` as a remote host ("Cannot connect to D:") — --force-local treats it as
# a local path. macOS bsdtar lacks that flag, so only pass it on Windows.
flags=""
[ "$RUNNER_OS" = "Windows" ] && flags="--force-local"
tar $flags -czpf "${RUNNER_TEMP}/electrobun-e2e-bundle.tgz" -C fixtures/e2e-apps/electrobun build

- name: 📦 Upload Electrobun E2E App Bundle
uses: actions/upload-artifact@v4
with:
name: electrobun-e2e-bundle-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/electrobun-e2e-bundle.tgz
retention-days: 1
if-no-files-found: error
Loading
Loading