diff --git a/.changeset/expo-verify-skill.md b/.changeset/expo-verify-skill.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-skill.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 0031ac9bc9c..a6ddf536741 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -23,9 +23,12 @@ Edits to a `SKILL.md` take effect immediately, including in already-running sess ## Scope -Skills are Claude Code specific. Cursor does not read this directory; it uses `.cursor/rules/` and -`AGENTS.md`. When a repo rule changes, update `AGENTS.md` first, then mirror the change here and in -`.cursor/rules/` where relevant. +Skills here are Claude Code specific, except `verify-clerk-expo`, the agent-neutral skill that drives +`@clerk/expo` on a simulator or emulator. Its files live here, and `.cursor/skills/verify-clerk-expo` +is a symlink to this directory so Cursor reads the same skill. Edit it here, not through the +symlink. For the other skills, Cursor uses `.cursor/rules/` and `AGENTS.md`. When a repo rule +changes, update `AGENTS.md` first, then mirror the change here and in `.cursor/rules/` where +relevant. ## Maintaining a skill @@ -41,7 +44,8 @@ Skills are Claude Code specific. Cursor does not read this directory; it uses `. ## Skills in this repo -| Skill | Use it for | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | -| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| Skill | Use it for | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | +| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, with video and app state as evidence. | diff --git a/.claude/skills/verify-clerk-expo/.gitignore b/.claude/skills/verify-clerk-expo/.gitignore new file mode 100644 index 00000000000..603ba938f6c --- /dev/null +++ b/.claude/skills/verify-clerk-expo/.gitignore @@ -0,0 +1,3 @@ +.e2e/ +.verify/ +specs/explored/ diff --git a/.claude/skills/verify-clerk-expo/SKILL.md b/.claude/skills/verify-clerk-expo/SKILL.md new file mode 100644 index 00000000000..fbe369857a5 --- /dev/null +++ b/.claude/skills/verify-clerk-expo/SKILL.md @@ -0,0 +1,220 @@ +--- +name: verify-clerk-expo +description: Drive @clerk/expo in the expo-native fixture app (native AuthView, UserButton, UserProfileView, custom useSignIn and useSignUp flows, token cache) on an iOS simulator or Android emulator against a real Clerk development instance that the session creates and deletes, and capture video, screenshots, and app state as evidence. The device is a simulator or emulator on this Mac. Use it to prove any change to packages/expo or the fixture works before calling it done, to reproduce a UI bug, or to run the golden regression specs. +--- + +# verify-clerk-expo + +`.claude/skills/verify-clerk-expo/bin/control-clerk-expo` is a control CLI over [e2e](https://github.com/tester-army/e2e) 0.15.2 and `@e2e-dev/mobile` 0.9.0. It builds the `expo-native` fixture in `integration/templates/expo-native`, leases a simulator or emulator, creates one Clerk application for the worktree, seeds `+clerk_test` users, runs specs, and keeps the evidence. The device is local, the fixture is a Debug dev client, and Metro serves your working tree to it. The skill needs a Mac: on any other machine `doctor`, `up`, and `run` fail with `UNSUPPORTED`. + +No change to `@clerk/expo` UI or auth behavior is done until a `run` on the real fixture shows the changed behavior, on each platform the change touches. + +Run every command from the repo root. In the prose below, `doctor`, `up`, `run`, `screen`, `attach`, and `down` are verbs of that CLI. Paths that begin `specs/`, `features/`, `references/`, `src/`, `test/`, or `.verify/` are inside `.claude/skills/verify-clerk-expo/`. Every verb but `attach` takes `--platform ios|android`, and iOS is the default. Every verb takes `--json` and then prints one `{ "ok": ... }` object. Exit codes are 0 for success, 1 for a failing spec, 2 for a usage error, and 3 for a failed precondition. Every error prints a `fix` line. + +CI runs none of these specs. The `Verify Skill Tests` job in `.github/workflows/ci.yml` runs the skill's unit tests and `tsc`. The device tests that gate a pull request are `integration/tests/expo-native/*.e2e.ts`, which `.github/workflows/expo-native-build.yml` runs against a Release build of the same fixture with the same `e2e` engine. This skill is the development loop, and a regression test that must run on every pull request belongs in `integration/tests/expo-native/`. + +The fixture links `@clerk/expo`, `@clerk/expo-biometrics`, and `@clerk/expo-google-signin` from the workspace. It does not install `@clerk/expo-passkeys`, so the skill cannot verify passkeys. + +## Launch + +Set up each machine once. + +1. Install Node 24. For iOS, install Xcode with an iOS simulator runtime. For Android, install Android Studio with the SDK, the emulator, and Java 21. The CLI looks for the SDK in `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and `~/Library/Android/sdk`. +2. Create the iOS template simulator, which the CLI clones to make each simulator it drives, for example with `xcrun simctl clone "iPhone Air" "Clerk Verify Template iOS"`. If this Mac sends HTTPS through a debugging proxy, boot the template once, install and trust the proxy's CA in it, and shut it down. Android needs no template: the first `up --platform android` writes the `Clerk_Verify_Pixel` AVD. +3. Give the machine the team's Clerk Platform API key. Set `CLERK_PLATFORM_API_KEY`, or set `CLERK_PLATFORM_API_KEY_FILE` to a file that only you can read (mode 0600). To keep the key in 1Password instead, install the 1Password CLI, turn on its desktop app integration, and put the key's secret reference in `VERIFY_PLATFORM_KEY_REFERENCE` or as the one line of `~/.verify/clerk-platform-key-reference`. The reference has the shape `op:////credential`, and the team's private setup note has the real one. Never put the key or the reference in a file inside a repository. + +Then, in each worktree: + +```console +$ pnpm install # once per worktree +$ npm ci --prefix .claude/skills/verify-clerk-expo # once per worktree +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo doctor --platform ios +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo up --platform ios +instance creating verify-throwaway-until-- in org_3KHungJxbvIscuSvy8oos5MHAli +build local building... +build turbo build @clerk/expo, @clerk/expo-biometrics, @clerk/expo-google-signin +instance up in 0.8s on standard, 212 settings match src/core/instances/base.json +build expo prebuild --clean --platform ios +build xcodebuild Debug (dev client) +build local built in 123s +device verify-ios-2 cloning Clerk Verify Template iOS +install on verify-ios-2 +watch packages/expo tsdown --watch (pid ) +metro :8083 expo start (pid ) +metro :8083 bundling ios once so the first launch does not wait on Metro +device verify-ios-2 local leased by this worktree installed +``` + +The skill is outside the pnpm workspace, so `npm ci` installs its pinned `e2e` and `agent-device` from the skill's own lockfile. The sample is the first `up` in a worktree, without its `instances` and `clerk` lines. The lane is ready when `up` prints the `device` line that ends in `installed `, which is its last line. `run` does the same steps itself, so `up` only starts the slow part early. Teardown is `down` (see [Cleanup](#cleanup)). + +`up` does four things: + +- It builds the dev client when no build matches the native inputs: `turbo build` for the three packages, `expo prebuild --clean`, then `xcodebuild` or `gradlew assembleDebug`. The build overwrites the fixture's generated `package.json`, `ios/`, and `android/`. A change to anything else reuses the build and prints `build local reused`. +- It creates this worktree's Clerk application through Clerk's Platform API, in the team's verification workspace, and puts its development instance on the standard settings in `src/core/instances/base.json`. The application holds only the users that this worktree's runs create, and `down` deletes it. [Test instances](references/instances.md) has the credential lookup, the application's lifetime, and its limits. +- It leases a lane and installs the build. An iOS lane is a clone of the template named `verify-ios-`. An Android lane boots `Clerk_Verify_Pixel` read-only as `emulator-5560` or `emulator-5562`. +- It starts `tsdown --watch` in `packages/expo` (the watch build) and `expo start` on the lane's Metro port. + +`up` is idempotent. It keeps a lease that this worktree already holds. A failed `up` stops the Metro and the watch build that it started. + +A JS change reaches the app with no build. Before the specs start, `run` waits until the watch build has caught up and Metro serves the current code, and it fails with `NOT_READY` and the path of the Metro log when Metro never does. [How a change reaches the app](references/freshness.md) has the native inputs, the checks, the ports, and the logs. It also says what to do after a change to another workspace package, such as `@clerk/clerk-js` or `@clerk/shared`. While Metro runs, never run `pnpm --filter @clerk/expo build` or a build of a package that `@clerk/expo` depends on, because the build deletes the `dist` that Metro serves. + +A worktree can hold one lane of each platform. The two lanes share the watch build and the application, and each has its own Metro. Start their runs one after the other, for two reasons. A `run` that finds an edited sibling package stops every Metro of the worktree while it rebuilds the package, including the Metro that a run on the other platform is using. And while both platforms ran specs on the shared application at the same time, a ticket sign-in failed with `resource_not_found` in two of four tries, which never happened with one run at a time. + +A Mac has four iOS lanes and two Android lanes, shared by every worktree on it. When all are taken, `up` and `run` fail with `POOL_FULL`, and `--wait ` on either verb waits for a lane. Never drive a simulator or emulator that the CLI did not create, the template, a physical device, or a lane that another worktree holds. [Local devices](references/devices.md) says how to find a lane's UDID or serial. + +With the key in 1Password, a command that needs it prints `wait reading the team key from 1Password; approve the request in the 1Password app within 60s`, and the 1Password app asks the person at the Mac to approve. An agent cannot approve the request, so tell the person before the first command. + +## Doctor + +```console +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo doctor --platform ios +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo doctor --platform android +``` + +Run it first, and again whenever anything looks off. Without `--live` it only reads. It creates no file, no device, and no Clerk application. Each line starts with `ok`, `warn`, `skip`, or `FAIL`, then has the id of the check and what the check found. `skip` marks a check that did not run, and its text starts with `not run:`. A failing check also prints a `fix:` line with the command to run, and `doctor` exits 3. A warning does not change the exit code. + +| Checks | Pass when | +| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `node`, `e2e-pins` | Node is 24.x, and the installed `e2e` and `@e2e-dev/mobile` are the versions that `package.json` pins. `e2e-pins` fails until `npm ci` has run in this worktree | +| `xcode`, `template`, `proxy-trust`, `lane-ports` | iOS: `xcodebuild` runs. `Clerk Verify Template iOS` exists and is shut down. It trusts a custom CA if macOS has a system HTTPS proxy. Every booted `verify-ios-` simulator has a live claim | +| `jdk`, `template`, `lane-ports` | Android: a Java 21 is found. The SDK's emulator and adb run. Ports 5560 to 5562 hold only lanes that the CLI booted | +| `instances`, `clerk-api`, `settings` | A Platform API credential reaches the verification workspace. Clerk's Backend API accepts the secret key of the application this worktree holds. That application shows the settings recorded for it, and every declaration under `specs/` is well formed | +| `build` | A build of the fixture matches the current tree | +| `gh-attach` | `gh pr comment` has `--attach`. A `gh` without it prints `warn` and not `FAIL`, because only `attach` needs it | +| `stale-claims`, `agent-device-daemon` | No lane is claimed by a worktree that no longer exists, and no agent-device daemon runs from an install that was deleted | +| `feature-map`, `core-drift` | Every feature in `src/host.ts` has a feature file and a golden spec, and `src/core/` matches `src/core/MANIFEST` | + +After the once-per-machine setup and before the first `up`, `build` is the one failing check, and its fix is the `up` command for that platform. A machine with no Platform API credential fails `instances`, and the fix line says how to supply one. `doctor --live` also proves that the credential can do the work. It creates one application, configures it, compares it with the standard file, and deletes it, and reports that in a `live-instance` line. When this worktree already holds an application, `doctor --live` creates nothing, and the `live-instance` line is a `skip`. + +## Drive + +Input reaches the app only through specs. A spec is a TypeScript file that uses the `host` fixture from `specs/fixtures.ts` and e2e's `screen` locators. + +```console +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo run native-auth-view --platform ios # one feature: every spec in specs/golden/native-auth-view/ +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo run custom-flow-sign-in/request-code # one golden spec, on iOS +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo run specs/explored/.e2e.ts # a spec you wrote +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo run native-auth-view native-js-sync # several targets in one run, with one video +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo run --all --skip form-entry --platform android # every golden spec except the ones that type a code or a password +$ .claude/skills/verify-clerk-expo/bin/control-clerk-expo screen --platform ios # the UI tree that is on screen now; --png adds a screenshot +``` + +`run` also takes `--grep `, `--no-video`, and `--wait `. The wait covers a free lane and another `run` in this worktree that holds the device. A golden spec tagged `known-bug` reproduces an open bug, and `run` leaves it out unless you pass `--include known-bug`. Today that is the AuthView dismiss test in `native-auth-view/opens`. A spec limited to one platform with `test(title, { platforms: ['ios'] }, fn)` reports as skipped on the other. + +For a JS change, edit the source and `run` the spec, with no `up` in between. For a change to a native input, the same `run` rebuilds the dev client first. + +### Sign in with the form or with a ticket + +A change that touches sign-in or sign-up gets a spec that drives the real form with a `+clerk_test` identity and the test code. Any other change reaches a signed-in state with a sign-in ticket, `host.launch({ signedInAs: user })`, which is the intended shortcut and not a fallback. A runtime that cannot type codes into the app skips the typing specs with `--skip form-entry` on `run` and says so in the PR. + +Tag every spec that types a code or a password `form-entry`. Four golden specs carry the tag: `custom-flow-sign-in/complete`, `custom-flow-sign-up/request-code`, `custom-flow-sign-up/complete`, and `native-auth-view/complete`. `run` reports a skipped one as `skipped by --skip form-entry`. Never report it as verified through a ticket launch. With the flag, `custom-flow-sign-in/request-code` and `native-auth-view/request-code` still run and prove their flow up to the code screen. Sign-up has no spec that runs without typing a password, so a run with the flag leaves sign-up unproven. + +Type only test identities: `+clerk_test` emails and the code `424242`, which specs import as `CLERK_TEST_CODE`. Never type a real person's address or password. The repository is public, and a run's video can land on a PR. [The feature map index](features/README.md) lists the test identities and the identifier for each step of the forms. + +### Write a spec + +```ts +import { test, expect } from '../../fixtures.ts'; +import { nativeProfile } from '../../native.ts'; + +test('the native UserProfileView shows the seeded user', async ({ host, screen }) => { + const user = await host.seedUser(); + const state = await host.launch({ signedInAs: user, screen: 'userProfile' }); + expect(state.userId).toBe(user.id); + expect(state.sessionStatus).toBe('active'); + await host.tap(nativeProfile(screen).manageAccount); + await expect(screen.getByText(user.email)).toBeVisible({ timeout: 20_000 }); + await host.screenshot('profile'); +}); +``` + +- `host.seedUser({ phone? })` creates a `+clerk_test` user in the worktree's application. `host.newEmail()` reserves an address for a sign-up through the form. +- `host.launch({ signedInAs?, screen?, authMode?, debugLogs?, keepStorage? })` relaunches the fixture and returns the first ready `VerifyState` of that launch. Screens are `home`, `auth` (AuthView with no close button), `nativeAuth` (AuthView with a close button), `userButton`, `userProfile`, `customSignIn`, `customSignUp`, and `tokenCache`. Auth modes are `signIn`, `signUp`, and `signInOrUp`. A launch starts with fresh storage unless `keepStorage` is true, so a launch with no `signedInAs` is signed out. A new screen goes in `verify/launch.ts` and `verify/VerifyHost.tsx` of the fixture, and in `SCREENS` in `src/host.ts`. +- `host.state()` reads the state footer, and `host.waitForState(predicate, timeoutMs?)` polls it. +- `host.tap(locator)` taps the middle of a node, and `host.fill(locator, text)` taps it and types. Use them inside the native views. agent-device reports a SwiftUI view inside the React Native host as covered by another element and refuses a plain `locator.tap()` or `locator.fill()` there. +- `host.screenshot(label)` writes `screenshots/