@@ -10,16 +10,9 @@ import { fileURLToPath } from "node:url";
1010// Vendored fork import (same pattern as mcporter).
1111import { createEmulator } from "@executor-js/emulate" ;
1212
13- import { bootProcesses , ensurePortsFree , waitForHttp } from "./boot" ;
14- import {
15- CLOUD_BASE_URL ,
16- CLOUD_DB_PORT ,
17- CLOUD_PORT ,
18- WORKOS_EMULATOR_PORT ,
19- AUTUMN_EMULATOR_PORT ,
20- E2E_WORKOS_CLIENT_ID ,
21- E2E_COOKIE_PASSWORD ,
22- } from "../targets/cloud" ;
13+ import { claimPorts } from "../src/ports" ;
14+ import { E2E_COOKIE_PASSWORD , E2E_WORKOS_CLIENT_ID } from "../targets/cloud" ;
15+ import { bootProcesses , waitForHttp } from "./boot" ;
2316
2417const cloudDir = fileURLToPath ( new URL ( "../../apps/cloud/" , import . meta. url ) ) ;
2518
@@ -29,12 +22,19 @@ export default async function setup(): Promise<(() => Promise<void>) | void> {
2922 return ;
3023 }
3124
32- await ensurePortsFree ( [
33- { port : CLOUD_PORT , label : "cloud vite dev" } ,
34- { port : CLOUD_DB_PORT , label : "cloud dev-db (PGlite)" } ,
35- { port : WORKOS_EMULATOR_PORT , label : "WorkOS emulator" } ,
36- { port : AUTUMN_EMULATOR_PORT , label : "Autumn emulator" } ,
25+ // Claim a free port block (preferred block first, walk forward past
26+ // squatters/colliding checkouts) and publish via env so the test workers —
27+ // spawned after this — derive the same URLs. The imported targets/cloud
28+ // constants were computed BEFORE the claim, so use the claimed values here.
29+ const { ports, release } = await claimPorts ( [
30+ { envVar : "E2E_CLOUD_PORT" , offset : 0 , label : "cloud vite dev" } ,
31+ { envVar : "E2E_CLOUD_DB_PORT" , offset : 1 , label : "cloud dev-db (PGlite)" } ,
32+ { envVar : "E2E_WORKOS_EMULATOR_PORT" , offset : 2 , label : "WorkOS emulator" } ,
33+ { envVar : "E2E_AUTUMN_EMULATOR_PORT" , offset : 3 , label : "Autumn emulator" } ,
3734 ] ) ;
35+ const cloudPort = ports . E2E_CLOUD_PORT ! ;
36+ const dbPort = ports . E2E_CLOUD_DB_PORT ! ;
37+ const baseUrl = `http://127.0.0.1:${ cloudPort } ` ;
3838
3939 // Fresh dev DB per suite run — hermetic, like the selfhost data dir. The
4040 // WorkOS emulator mints org ids from a per-process counter, so a persisted
@@ -46,8 +46,8 @@ export default async function setup(): Promise<(() => Promise<void>) | void> {
4646 // MCP access tokens minted by the emulator's OAuth server must carry the
4747 // app's client id as audience (what the resource server verifies).
4848 process . env . EMULATE_WORKOS_AUDIENCE = E2E_WORKOS_CLIENT_ID ;
49- const workos = await createEmulator ( { service : "workos" , port : WORKOS_EMULATOR_PORT } ) ;
50- const autumn = await createEmulator ( { service : "autumn" , port : AUTUMN_EMULATOR_PORT } ) ;
49+ const workos = await createEmulator ( { service : "workos" , port : ports . E2E_WORKOS_EMULATOR_PORT ! } ) ;
50+ const autumn = await createEmulator ( { service : "autumn" , port : ports . E2E_AUTUMN_EMULATOR_PORT ! } ) ;
5151
5252 const env = {
5353 // Real client, emulated service.
@@ -58,16 +58,16 @@ export default async function setup(): Promise<(() => Promise<void>) | void> {
5858 WORKOS_COOKIE_PASSWORD : E2E_COOKIE_PASSWORD ,
5959 AUTUMN_SECRET_KEY : "am_test_emulate" ,
6060 ENCRYPTION_KEY : "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ,
61- DATABASE_URL : `postgresql://postgres:postgres@127.0.0.1:${ CLOUD_DB_PORT } /postgres` ,
61+ DATABASE_URL : `postgresql://postgres:postgres@127.0.0.1:${ dbPort } /postgres` ,
6262 EXECUTOR_DIRECT_DATABASE_URL : "true" ,
6363 CLOUDFLARE_INCLUDE_PROCESS_ENV : "true" ,
64- VITE_PUBLIC_SITE_URL : CLOUD_BASE_URL ,
64+ VITE_PUBLIC_SITE_URL : baseUrl ,
6565 // The AuthKit domain (MCP OAuth metadata + JWKS) is the emulator too.
6666 MCP_AUTHKIT_DOMAIN : workos . url ,
67- MCP_RESOURCE_ORIGIN : CLOUD_BASE_URL ,
67+ MCP_RESOURCE_ORIGIN : baseUrl ,
6868 ALLOW_LOCAL_NETWORK : "true" ,
6969 // Throwaway PGlite on its own port + dir so it never fights `bun dev`.
70- DEV_DB_PORT : String ( CLOUD_DB_PORT ) ,
70+ DEV_DB_PORT : String ( dbPort ) ,
7171 DEV_DB_PATH : dbPath ,
7272 } ;
7373
@@ -76,7 +76,7 @@ export default async function setup(): Promise<(() => Promise<void>) | void> {
7676 { cmd : "bun" , args : [ "run" , "scripts/dev-db.ts" ] , cwd : cloudDir , env } ,
7777 {
7878 cmd : "bunx" ,
79- args : [ "vite" , "dev" , "--port" , String ( CLOUD_PORT ) , "--strictPort" , "--host" , "127.0.0.1" ] ,
79+ args : [ "vite" , "dev" , "--port" , String ( cloudPort ) , "--strictPort" , "--host" , "127.0.0.1" ] ,
8080 cwd : cloudDir ,
8181 env,
8282 } ,
@@ -85,18 +85,20 @@ export default async function setup(): Promise<(() => Promise<void>) | void> {
8585 ) ;
8686
8787 try {
88- await waitForHttp ( CLOUD_BASE_URL ) ;
88+ await waitForHttp ( baseUrl ) ;
8989 // The API plane is ready when login actually redirects to AuthKit.
90- await waitForHttp ( `${ CLOUD_BASE_URL } /api/auth/login` , { expectRedirect : true } ) ;
90+ await waitForHttp ( `${ baseUrl } /api/auth/login` , { expectRedirect : true } ) ;
9191 } catch ( error ) {
9292 await procs . teardown ( ) ;
9393 await workos . close ( ) ;
9494 await autumn . close ( ) ;
95+ await release ( ) ;
9596 throw error ;
9697 }
9798 return async ( ) => {
9899 await procs . teardown ( ) ;
99100 await workos . close ( ) ;
100101 await autumn . close ( ) ;
102+ await release ( ) ;
101103 } ;
102104}
0 commit comments