Skip to content

test(expo): add a verify skill that drives the expo-native fixture - #10053

Draft
mikepitre wants to merge 23 commits into
mike/expo-verify-hostfrom
mike/expo-verify-skill
Draft

mikepitre wants to merge 23 commits into
mike/expo-verify-hostfrom
mike/expo-verify-skill

Conversation

@mikepitre

@mikepitre mikepitre commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds verify-clerk-expo, a verification skill for @clerk/expo at .claude/skills/verify-clerk-expo/. An agent changing packages/expo can use it to prove the change on a real iOS simulator or Android emulator against a real Clerk dev instance, and attach the video to the PR. It builds on the expo-native host from #10052, and has the same layout and verbs as the clerk-ios and clerk-android verification skills.

Any agent finds it from the new Verifying changes section in AGENTS.md. .cursor/skills/verify-clerk-expo is a committed relative symlink to the same directory for Cursor; there is one copy of the files. The CLI is .claude/skills/verify-clerk-expo/bin/control-clerk-expo, and it runs from any directory. The skill is named verify-clerk-expo, not verify, so it leaves Claude Code's built-in /verify alone.

What an agent gets:

  • control-clerk-expo up --platform ios|android builds the fixture as a Debug dev client, leases a lane device, and installs the app. It also starts tsdown --watch in packages/expo and an expo start on the lane's own Metro port. iOS lanes use 8082 to 8085 and Android lanes use 8086 and 8087, so lanes never share a port.
  • The build key covers only native inputs: packages/expo/ios or android, app.plugin.js, src/specs/, expo-module.config.json, package.json, the native code and package.json of expo-google-signin and expo-biometrics, and the fixture's app config, pnpm-workspace.yaml, and local module. A change under packages/expo/src reuses the dev client and reaches the app on the next launch through the watch build and Metro. A native change rebuilds it.
  • Scope: the skill builds and serves @clerk/expo and its Expo-module siblings (expo-biometrics, expo-google-signin, expo-passkeys) by itself. Changes to its other workspace dependencies (@clerk/clerk-js, @clerk/shared, @clerk/react) are verified after one build step the developer runs: when any of them, or a workspace package it bundles, has source newer than its dist, up and run refuse with NOT_READY before anything launches and print pnpm turbo build --filter=@clerk/expo^....
  • Before every launch, run checks that the app will load current JS. Stale Expo-module siblings are rebuilt with Metro stopped. The @clerk/expo watch build must have caught up and settled. Then run fingerprints the content of every bundled dist file of @clerk/expo and its workspace dependencies, fetches the dev client's bundle from Metro, and fingerprints again. Each Metro revision is remembered with the content it was first seen with, so a revision that lags a newer edit is never confirmed, and a Metro whose first bundle cannot be dated against the current dist is restarted. Two matching reads are required. A rewrite with identical content waits for nothing. A 500 while dist is being rewritten is retried; a bundling error fails once the outputs have settled. When Metro's watcher misses a write, run touches the files after 5 seconds. If Metro never catches up, run fails with NOT_READY and names the Metro log, instead of running specs against old code. up bundles once before it reports ready, and a runtime step that fails stops the Metro and watch build that call started. Metro runs with EXPO_OFFLINE=1, because the Expo CLI's calls to expo.dev went through this Mac's HTTPS proxy and timed out.
  • control-clerk-expo run <feature> runs the golden specs for that feature with video, screenshots, and every verify.state the host reported. The features are native-auth-view, user-button-and-profile, custom-flow-sign-in, custom-flow-sign-up, token-cache-persistence, and native-js-sync.
  • The auth specs drive the real forms with +clerk_test users. The request-code specs stop at the code screen. The complete specs type the published test code and are tagged form-entry, so a runtime that must not type codes can pass --skip form-entry.
  • control-clerk-expo down releases the devices, stops Metro and the watch build, and deletes the users the run created. It keeps the evidence.
  • A spec tagged known-bug proves a defect that is not fixed yet and is skipped unless --include known-bug is passed. The inline AuthView dismiss test carries it today: on iOS, the close button of an inline dismissible AuthView does not fire onDismiss.

The complete specs were authored here but not run by this PR's author, whose agent runtime does not type sign-in codes into an app that talks to a hosted Clerk instance. Every auth flow was proven up to its code screen with --skip form-entry, and the complete specs are left for CI or a runtime that allows form entry.

src/core/, src/platform/ios/, and specs/fixtures.ts are byte-identical copies from clerk/clerk-ios#624. src/platform/android/ is byte-identical to the clerk-android skill in clerk/clerk-android#1042. src/host.ts, specs/native.ts, the golden specs, and features/ are specific to Expo. The skill's own .gitignore covers its .verify/ state and evidence and its specs/explored/, so the root .gitignore is unchanged. .prettierignore skips the skill's .ts files, because the shared copies keep the clerk-ios formatting. Markdown and JSON in the skill still go through Prettier. The skill sits outside every workspace package, so root lint, root format, and @clerk/expo's tests never see it. .claude/skills/README.md says this skill is agent-neutral and that Cursor reads it through the symlink.

Android specs find native views by text, because the clerk-android release that @clerk/expo pins has no test tags yet. iOS specs use the clerk.* accessibility identifiers. On Android the runtime also marks the expo-dev-menu onboarding finished, because a -read-only emulator forgets it on every boot. On iOS, app.log keeps the fixture process's own native log lines; JS console lines from a Debug dev client go to Metro, and the skill points there.

test/freshness.test.ts covers the gate's decisions and drives its loop with a fake Metro: a lagging revision after a second edit, a transient 500 while dist refills, a settled 500, a Metro restart, the touch for a missed watcher event, and the scope split. No live run has needed the touch yet. Metro and watch logs append across restarts; copying each run's slice of the Metro log into its run directory would need a hook in the shared core, so iOS JS log lines are read from .verify/runtime/ by runId.

down --platform <p> releases only that platform's lane but stops the worktree's whole runtime, because core's process ledger entries don't record a platform. The skill documents this. Recording the platform on those entries is a follow-up for the shared core in clerk/clerk-ios#624, which every repo copies.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other: test tooling

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5253fee

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Oct 3, 2026 12:50pm UTC
swingset Ready Ready Preview Oct 3, 2026 12:50pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

mikepitre and others added 3 commits October 3, 2026 04:27
The skill builds the fixture as a Debug dev client, leases a lane
simulator or emulator, serves packages/expo through a watch build and
Metro, and runs golden specs for the native AuthView, the native
profile, the custom useSignIn and useSignUp flows, the token cache, and
native-to-JS sign-out. src/core, src/platform, and specs/fixtures.ts are
shared byte for byte with clerk-ios and clerk-android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It keeps ReactNativeJS lines in app.log, where the Expo host logs its
verify state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom clerk-ios 69c1d6a2

The new core starts the host runtime on up and run, launches dev clients
through am start on Android, and ORs the React Native log subsystem into
the iOS app.log filter. The Android lane layer comes from clerk-android
43a319c4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the iOS strong-password sheet in sign-up specs

The Android lane layer now comes from clerk-android 1811e589.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v menu out of the way

An Android build no longer stops another lane's Metro while the watch
build keeps packages/expo/dist current. The Android runtime marks the
dev menu onboarding finished, which a -read-only emulator forgets on
every boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikepitre and others added 2 commits October 3, 2026 05:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Expo logs land

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It binds the agent-device session after an Android dev-client start and
reports platform-scoped specs as skipped on the other platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Metro before the first launch

The runtime hook now fetches the dev client's bundle URL before every
launch. It waits until Metro reports a revision that includes the dist
files changed since the last launch, and rewrites them when Metro's file
watcher missed the write. up bundles once so the first launch after an
install never waits on a cold Metro. The watch catch-up check counts
only the files tsdown builds, a stale @clerk/expo-biometrics dist is
rebuilt, and the native-input key covers the sibling package.json files
and the fixture's pnpm-workspace.yaml. iOS app.log now keeps the fixture
process's own lines. The Android lane layer comes from clerk-android
34a52426.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d when up fails

The freshness gate now fingerprints the content of every bundled dist
file of @clerk/expo and its workspace dependencies, and waits for a new
Metro revision only when content the bundle includes changed. An
identical rewrite no longer wedges a run, and a write that lands during
the fetch is caught by fingerprinting before and after it. A missed
watcher event is nudged with utimes instead of a rewrite. Stale
workspace dependencies are rebuilt with this worktree's Metro and watch
build stopped. The runtime hook stops what it started when it fails,
retries transient manifest and bundle errors with backoff, fails at once
on a bundling error, and runs Metro with EXPO_OFFLINE so the Expo CLI
does not call expo.dev through the system proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skill now sits at the repo root as verify-clerk-expo, where pstack's
create-verification-skill and maintain-verification-skill expect it,
and AGENTS.md points every agent at it. host.ts resolves the repo root
from the new depth, the @clerk/expo vitest config no longer needs an
exclude, and core comes from clerk-ios 84099924.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…skill for Claude Code

.claude/skills/verify-clerk-expo is a committed symlink to the one real
copy in .cursor/skills/verify-clerk-expo, so Claude Code and Cursor read
the same files. The docs call the CLI by its path from the repo root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tAdapter.cli

Core now prints control-clerk-expo's path in usage, fix, and next text.
host.ts writes the {cli} token in its own messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its fix text now names this skill's CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mikepitre and others added 2 commits October 3, 2026 07:44
The skill's files now live in .claude/skills/verify-clerk-expo. The CLI
path and every doc follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sor reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inst the content it was first seen with

up and run now refuse with NOT_READY when a workspace dependency outside
@clerk/expo and its Expo-module siblings has source newer than its dist,
instead of rebuilding it. The freshness gate records the content each
Metro revision was first seen with, so it never confirms a revision that
lags a newer edit, restarts a Metro whose first bundle cannot be dated,
waits out a 500 while dist is being rewritten, and re-lists the outputs
on every poll. Metro and watch logs append instead of truncating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fter one build step

The refusal names the build that covers every dependency of @clerk/expo,
and the docs say the gate then waits until Metro serves the rebuilt
code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing error settle before failing

A dependency counts as stale only when its source content differs from
the content its dist was last seen built from, so a touch that changes
nothing no longer refuses or rebuilds. Builds the CLI asks for use
--force, so they rewrite dist even on a turbo cache hit. A Metro 500 now
has to persist for 3 seconds over unchanged outputs before the gate
fails. The gate fingerprints dist before it restarts Metro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – swingset — 5253fee5 Deployed Oct 3, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 5253fee5 Deployed Oct 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant