Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 16 additions & 22 deletions .claude/rules/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description: E2E test instructions and required env vars
paths:
- "test/e2e/**"
- "scripts/run-tests.ts"
- "scripts/run-e2e-op.ts"
- "scripts/refresh-e2e-fixtures.ts"
alwaysApply: false
---
Expand Down Expand Up @@ -39,18 +39,21 @@ E2E_HAR_DIR=<path> # Directory to write HAR files per fixture
Preferred (secrets resolved from 1Password, no plaintext on disk):

```sh
bun run test:e2e:op # Run all fixture tests (concurrency defaults to CPU count)
bun run test:e2e:op -- --concurrency 1 # Serialize
bun run test:e2e:op -- --filter react # Only files matching "react"
bun run test:e2e:op -- --debug # Verbose helper logging (CLERK_E2E_DEBUG=1)
bun run test:e2e:op -- --har # Capture HAR files to test/e2e/.har
bun run test:e2e:op -- --har-dir ./out # Capture HAR files to a custom directory
bun run test:e2e:op # Run all fixture tests (workers default to CPU count)
CLERK_E2E_DEBUG=1 bun run test:e2e:op # Verbose helper logging
E2E_HAR_DIR=test/e2e/.har bun run test:e2e:op # Capture HAR files for network debugging
```

Extra args after `--` are forwarded to the underlying `bun test` invocation (see `bun test --help`). Note that additional positional patterns are OR'd with the existing `test/e2e/` pattern, so they broaden the run rather than narrow it — to run a single fixture, invoke `bun test` directly against its test file (the file is `test/e2e/<name>.test.ts`, not the `test/e2e/fixtures/<name>/` project dir) with the required env vars exported:

```sh
bun test ./test/e2e/react.test.ts --parallel --retry 1
```

Direct (CI / contributors without 1Password — env vars must already be set):

```sh
bun run test:e2e # Same flags as above
bun run test:e2e
```

Fixture maintenance:
Expand All @@ -61,18 +64,9 @@ bun run e2e:refresh-fixtures -- --force # Include pinned fixtures
bun run e2e:refresh-fixtures -- --only nextjs-app-router # Refresh one fixture
```

## Test runner (`scripts/run-tests.ts`)

A single test runner used by both `bun run test` and `bun run test:e2e`. Each test file runs as a separate `bun test` subprocess to avoid shared process state (env vars, module singletons). The runner supports:
## Test runner

- `--pattern <glob>` (required, repeatable): glob patterns to discover test files
- `--exclude <glob>` (repeatable): glob patterns to exclude matched files
- `--concurrency <n>` (default: CPU count): how many test files run in parallel
- `--filter <string>`: only run files whose path contains the string
- `--retries <n>` (default 0): automatic retries on failure (e2e uses 1 for transient FAPI throttling, Playwright timeouts)
- `--debug`: forwards `CLERK_E2E_DEBUG=1` to each test subprocess
- `--har`: forwards `E2E_HAR_DIR=test/e2e/.har` to each test subprocess (creates the dir if missing)
- `--har-dir <path>`: same as `--har` but writes HAR files to a custom directory
Both `bun run test` and `bun run test:e2e` invoke `bun test` with `--parallel`, which runs each test file in an isolated worker process (implies `--isolate`) to avoid shared process state (env vars, module singletons, `mock.module()` registrations). `--parallel` requires Bun >= 1.3.13 and is silently ignored by older versions, so both scripts first run `scripts/check-bun-version.ts` to fail fast on an outdated Bun. `test:e2e` also passes `--retry 1` for transient FAPI throttling and Playwright timeouts.

## How fixtures work

Expand Down Expand Up @@ -119,7 +113,7 @@ On failure: takes a screenshot to `/tmp/clerk-e2e-<name>-failure.png` and logs d

### Playwright patch

`playwright-core` is patched via `patchedDependencies` in `package.json` to work around a `route.fetch()` incompatibility under Bun. The patch file lives at `patches/playwright-core@1.58.2.patch`.
`playwright-core` is patched via `patchedDependencies` in `package.json` to work around a `route.fetch()` incompatibility under Bun. The patch file lives at `patches/playwright-core@1.60.0.patch`.

### Additional dependency

Expand All @@ -129,7 +123,7 @@ In CI, use `bunx playwright install chromium --with-deps` to include system-leve

## Concurrency

Fixture files run in parallel (concurrency controlled by the runner, defaults to CPU count). Each fixture uses an isolated temp directory and `CLERK_CONFIG_DIR`, so there is no shared mutable state. Do not use `test.concurrent` within individual fixture files.
Fixture files run in parallel (`bun test --parallel` worker count defaults to CPU count). Each fixture uses an isolated temp directory and `CLERK_CONFIG_DIR`, so there is no shared mutable state. Do not use `test.concurrent` within individual fixture files.

Within each test file, `createFixtureHarness()` runs `setupFixture()` once in `beforeAll` and shares the result with both the build test and browser test. This avoids duplicating the expensive setup.

Expand All @@ -155,6 +149,6 @@ E2E tests run in the `test-e2e` job in `.github/workflows/ci.yml`. Key details:

- Only runs for PRs from the same repository (skipped for external forks)
- Runs on `blacksmith-8vcpu-ubuntu-2404` with a 30-minute timeout
- Requires Node.js 22 (for Playwright) alongside Bun
- Requires Node.js 24 (for Playwright) alongside Bun
- Secrets `CLERK_CLI_TEST_APP_ID`, `CLERK_PLATFORM_API_KEY` are injected from GitHub Actions secrets
- Targets the production Clerk API (no `CLERK_PLATFORM_API_URL` / `CLERK_BACKEND_API_URL` overrides are set, so the defaults in `packages/cli-core/src/lib/environment.ts` apply). The local `bun run test:e2e:op` flow likewise resolves secrets from the `Clerk CLI - E2E Production Secrets` 1Password item. Test users are created with the `+clerk_test` email suffix and torn down at the end of each fixture run.
2 changes: 1 addition & 1 deletion .claude/rules/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Every top-level script follows this order:
4. **Core logic** -- `Bun.spawn` for async subprocesses, `Bun.spawnSync` for synchronous. Check exit codes explicitly. Use `try/finally` when temporary state (config files, temp dirs) needs cleanup.
5. **Exit handling** -- call `process.exit(1)` for expected failures (e.g., "no test files found"). Do not throw unhandled errors for anticipated failure paths.

See `scripts/run-tests.ts` as a reference implementation.
See `scripts/refresh-e2e-fixtures.ts` as a reference implementation.

## `lib/` utility conventions

Expand Down
4 changes: 2 additions & 2 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ Run the unit and integration test suite with:
bun run test
```

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).
This runs `bun test` with `--parallel`, which executes each test file in its own worker process, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.
When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. These flags require Bun >= 1.3.13 — older versions silently ignore them — so `bun run test` and `bun run test:e2e` first run `scripts/check-bun-version.ts`, which fails fast when the installed Bun is older than the `engines.bun` floor in package.json.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ bun run test:e2e # Run E2E tests with env vars already set (used by CI)

Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-memory and never written to disk. `bun run test:e2e` is for CI or for cases where the required env vars are already exported.

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.
CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun run test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.
When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already passes `--parallel`.

These flags require Bun >= 1.3.13 — older versions silently ignore them and lose isolation. `bun run test` and `bun run test:e2e` run `scripts/check-bun-version.ts` first, which fails fast when the installed Bun is older than the `engines.bun` floor in package.json.

## Versioning

Expand Down
22 changes: 10 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,37 +68,35 @@ bun run test:e2e # Run E2E tests with env vars already set (CI / non-1Passwo

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:
When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner. Run the unit suite with:

```sh
bun test
bun run test
```

Check for existing `*.test.ts` files near the code you're modifying.
Prefer `bun run test` over a bare `bun test`: the script passes `--parallel` (per-file isolation), scopes the run to the unit suite, and preflights the required Bun version first. Check for existing `*.test.ts` files near the code you're modifying.

### E2E tests

E2E tests verify that `clerk init` produces a buildable, type-safe project with working browser auth for each supported framework (Next.js, React, Vue, Nuxt, Astro, React Router, TanStack Start). They require a Clerk staging application and credentials.

**Locally, prefer `bun run test:e2e:op`.** It wraps `test:e2e` in `op run`, which resolves the required secrets from 1Password in-memory so nothing ever lands on disk. Any flags you pass are forwarded to the underlying runner:
**Locally, prefer `bun run test:e2e:op`.** It wraps `test:e2e` in `op run`, which resolves the required secrets from 1Password in-memory so nothing ever lands on disk:

```sh
# Install browser (only required once)
bunx playwright install chromium

bun run test:e2e:op # Run all E2E tests (secrets from 1Password)
bun run test:e2e:op -- --filter react # Run only tests matching "react"
bun run test:e2e:op -- --debug # Force serial execution for parsing logs (sets CLERK_E2E_DEBUG=1)
bun run test:e2e:op -- --har # Capture HAR files to test/e2e/.har for network debugging
bun run test:e2e:op -- --har-dir ./out # Capture HAR files to a custom directory
bun run e2e:refresh-fixtures # Re-scaffold fixture projects from upstream CLIs
bun run test:e2e:op # Run all E2E tests (secrets from 1Password)
CLERK_E2E_DEBUG=1 bun run test:e2e:op # Verbose helper logging
E2E_HAR_DIR=test/e2e/.har bun run test:e2e:op # Capture HAR files for network debugging
bun run e2e:refresh-fixtures # Re-scaffold fixture projects from upstream CLIs
```

If you already have the required env vars exported (e.g. in CI, or you don't have access to the 1Password vault), use `bun run test:e2e` directly instead. The flags are identical:
Args after `--` are forwarded to `bun test`, not to a custom runner — see `bun test --help`. Note that an extra positional pattern is OR'd with the existing `test/e2e/` pattern, so it broadens the run rather than narrowing it; to run a single fixture, invoke `bun test` directly against its test file with the env vars exported:

```sh
# Required env vars: CLERK_PLATFORM_API_KEY and CLERK_CLI_TEST_APP_ID
bun run test:e2e -- --filter react
bun test ./test/e2e/react.test.ts --parallel --retry 1
```

E2E test files live in `test/e2e/`, with fixture projects in `test/e2e/fixtures/`. Each test file exports a `FixtureConfig` and calls `runFixtureTest()` and `runBrowserTest()` from `test/e2e/lib/`. See `.claude/rules/e2e.md` for full details on adding fixtures and required env vars.
Expand Down
2 changes: 1 addition & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ bun run scripts/build.ts --target=bun-darwin-arm64

The `dev` and `start` commands do not inject a version (falls back to `0.0.0-dev`). The release workflow handles version injection.

> **Bun version for local rehearsal:** CI pins Bun `1.3.11` in [`build-binaries.yml`](../.github/workflows/build-binaries.yml) because `1.3.12` produces darwin-arm64 binaries that macOS codesign rejects. If you are rehearsing a release locally and plan to execute the compiled darwin-arm64 binary, match the CI pin. This will stop being relevant once the pin is lifted.
> **Bun version for local rehearsal:** CI pins Bun `1.3.11` in [`build-binaries.yml`](../.github/workflows/build-binaries.yml) because `1.3.12` produces darwin-arm64 binaries that macOS codesign rejects. If you are rehearsing a release locally and plan to execute the compiled darwin-arm64 binary, match the CI pin. This will stop being relevant once the pin is lifted. Note the pin only covers compiling binaries — it sits below the workspace's `engines.bun` floor (`>=1.3.13`), which is what running the test suite requires (`bun test --parallel` support).

### Releaser dry-run

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"scripts": {
"build": "bun run --filter @clerk/cli-core build",
"dev": "bun run --cwd packages/cli-core dev",
"test": "bun test 'packages/cli-core/src/' 'packages/extras/src/' 'scripts/' --parallel --only-failures",
"test:e2e": "bun test 'test/e2e/' --retry 1 --parallel --only-failures",
"test": "bun run scripts/check-bun-version.ts && bun test 'packages/cli-core/src/' 'packages/extras/src/' 'scripts/' --parallel --only-failures",
"test:e2e": "bun run scripts/check-bun-version.ts && bun test 'test/e2e/' --retry 1 --parallel --only-failures",
"test:e2e:op": "bun run scripts/run-e2e-op.ts",
"e2e:refresh-fixtures": "bun run scripts/refresh-e2e-fixtures.ts",
"typecheck": "bun run --filter './packages/*' typecheck && tsc --noEmit -p scripts/tsconfig.json && tsc --noEmit -p test/e2e/tsconfig.json",
Expand Down Expand Up @@ -50,7 +50,7 @@
"*.{md,json,css}": "oxfmt --write --no-error-on-unmatched-pattern"
},
"engines": {
"bun": ">=1.3.10"
"bun": ">=1.3.13"
},
"patchedDependencies": {
"playwright-core@1.60.0": "patches/playwright-core@1.60.0.patch"
Expand Down
28 changes: 28 additions & 0 deletions scripts/check-bun-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { test, expect, describe } from "bun:test";
import { checkBunVersion } from "./check-bun-version.ts";

describe("checkBunVersion", () => {
test.each([
["1.3.13", ">=1.3.13", true],
["1.4.0", ">=1.3.13", true],
["2.0.0", ">=1.3.13", true],
["1.4.0-canary.20260101", ">=1.3.13", true],
["1.3.12", ">=1.3.13", false],
["1.3.11", ">=1.3.13", false],
["1.2.0", ">=1.3.13", false],
])("version %s against %s -> ok=%p", (current, range, ok) => {
expect(checkBunVersion(current, range).ok).toBe(ok);
});

test("failure message names the required range and the found version", () => {
const result = checkBunVersion("1.3.11", ">=1.3.13");
expect(result.ok).toBe(false);
expect(result.message).toContain(">=1.3.13");
expect(result.message).toContain("1.3.11");
expect(result.message).toContain("bun upgrade");
});

test("success carries no message", () => {
expect(checkBunVersion("1.3.13", ">=1.3.13").message).toBeUndefined();
});
});
55 changes: 55 additions & 0 deletions scripts/check-bun-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Verify the running Bun satisfies the workspace's `engines.bun` range
* before the test suite starts.
*
* The test scripts rely on `bun test --parallel` (which implies `--isolate`)
* to run each test file in its own worker process. The flag only exists in
* Bun >= 1.3.13; older versions silently ignore it, run every test file in
* one process, and let `mock.module()` registrations bleed across files,
* producing hundreds of order-dependent failures. Bun does not enforce
* `engines.bun` at install time, so this preflight fails loudly instead.
*
* Usage:
* bun run scripts/check-bun-version.ts
*/

import { join } from "node:path";
import semver from "semver";

export interface CheckBunVersionResult {
ok: boolean;
message?: string;
}

export function checkBunVersion(current: string, range: string): CheckBunVersionResult {
// includePrerelease so canary builds of a satisfying version pass.
if (semver.satisfies(current, range, { includePrerelease: true })) {
return { ok: true };
}
return {
ok: false,
message: [
`error: this workspace requires Bun ${range}, but found ${current}.`,
"",
"The test scripts pass --parallel to `bun test` for per-file isolation.",
"Older Bun versions silently ignore the flag, causing mock.module()",
"registrations to leak across test files and fail unrelated tests.",
"",
"Upgrade with: bun upgrade",
].join("\n"),
};
}

if (import.meta.main) {
const pkg = await Bun.file(join(import.meta.dir, "../package.json")).json();
const range: string | undefined = pkg.engines?.bun;
if (!range) {
console.error("error: no `engines.bun` range found in the root package.json.");
process.exit(1);
}
const result = checkBunVersion(Bun.version, range);
if (!result.ok) {
console.error(result.message);
process.exit(1);
}
}
8 changes: 6 additions & 2 deletions scripts/run-e2e-op.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
* disk.
*
* Usage:
* bun run scripts/run-e2e-op.ts # run the full suite
* bun run scripts/run-e2e-op.ts -- --filter react # forward args to test:e2e
* bun run scripts/run-e2e-op.ts # run the full suite
* bun run scripts/run-e2e-op.ts -- -t "project builds" # forward flags to `bun test`
*
* Args after `--` are forwarded to `bun test`, not a custom runner. The suite's
* `test/e2e/` pattern is fixed, so a forwarded positional path is OR'd with it
* (broadening the run); use flags like `-t <name>` to narrow instead.
*/

import { ensureOpInstalled, runWithOpSecrets } from "./lib/op.ts";
Expand Down