From fe0ed0c6696042e8017c852db0d3899db034a9f9 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 20 Mar 2026 17:57:39 -0700 Subject: [PATCH 01/11] Add preliminary environment mode for Matrix tests --- mise-tasks/lib/env-vars.sh | 24 +++++++ mise-tasks/test-matrix | 20 ++++++ packages/matrix/docker/smtp4dev.ts | 49 ++++++++++--- packages/matrix/helpers/environment-config.ts | 26 +++++-- packages/matrix/helpers/index.ts | 21 ++++-- .../matrix/helpers/isolated-realm-server.ts | 68 +++++++++++++++---- packages/matrix/playwright.config.ts | 11 ++- packages/matrix/scripts/test.sh | 4 +- packages/matrix/tests/global.setup.ts | 12 +++- packages/matrix/tests/head-tags.spec.ts | 6 +- packages/matrix/tests/host-mode.spec.ts | 14 ++-- packages/matrix/tests/publish-realm.spec.ts | 19 +++--- .../tests/registration-with-token.spec.ts | 6 +- packages/matrix/tests/skills.spec.ts | 8 +-- 14 files changed, 219 insertions(+), 69 deletions(-) create mode 100755 mise-tasks/test-matrix diff --git a/mise-tasks/lib/env-vars.sh b/mise-tasks/lib/env-vars.sh index b5738f5b818..637d3d3f9e6 100755 --- a/mise-tasks/lib/env-vars.sh +++ b/mise-tasks/lib/env-vars.sh @@ -54,6 +54,14 @@ if [ -n "${BOXEL_ENVIRONMENT:-}" ]; then # Paths export REALMS_ROOT="./realms/${ENV_SLUG}" export REALMS_TEST_ROOT="./realms/${ENV_SLUG}_test" + + # Matrix test services (isolated realm server + worker for Playwright tests) + export MATRIX_TEST_REALM_URL="http://realm-matrix-test.${ENV_SLUG}.localhost" + export MATRIX_TEST_REALM_PORT=0 + export MATRIX_TEST_WORKER_PORT=0 + export MATRIX_TEST_PUBLISHED_DOMAIN="realm-matrix-test.${ENV_SLUG}.localhost" + export SMTP_URL="http://smtp.${ENV_SLUG}.localhost" + export SMTP_PORT=0 else # Capture previous ENV_MODE before resetting it, so we can detect transitions _PREV_ENV_MODE="${ENV_MODE:-}" @@ -91,6 +99,14 @@ else # Paths export REALMS_ROOT="./realms/localhost_4201" export REALMS_TEST_ROOT="./realms/localhost_4202" + + # Matrix test services + export MATRIX_TEST_REALM_URL="http://localhost:4205" + export MATRIX_TEST_REALM_PORT=4205 + export MATRIX_TEST_WORKER_PORT=4232 + export MATRIX_TEST_PUBLISHED_DOMAIN="localhost:4205" + export SMTP_URL="http://localhost:5001" + export SMTP_PORT=5001 else # Fresh standard mode or non-env-mode shell: # use :- so production/staging env vars are not clobbered. @@ -122,6 +138,14 @@ else # Paths export REALMS_ROOT="${REALMS_ROOT:-./realms/localhost_4201}" export REALMS_TEST_ROOT="${REALMS_TEST_ROOT:-./realms/localhost_4202}" + + # Matrix test services + export MATRIX_TEST_REALM_URL="${MATRIX_TEST_REALM_URL:-http://localhost:4205}" + export MATRIX_TEST_REALM_PORT="${MATRIX_TEST_REALM_PORT:-4205}" + export MATRIX_TEST_WORKER_PORT="${MATRIX_TEST_WORKER_PORT:-4232}" + export MATRIX_TEST_PUBLISHED_DOMAIN="${MATRIX_TEST_PUBLISHED_DOMAIN:-localhost:4205}" + export SMTP_URL="${SMTP_URL:-http://localhost:5001}" + export SMTP_PORT="${SMTP_PORT:-5001}" fi unset _PREV_ENV_MODE diff --git a/mise-tasks/test-matrix b/mise-tasks/test-matrix new file mode 100755 index 00000000000..2c27464fd37 --- /dev/null +++ b/mise-tasks/test-matrix @@ -0,0 +1,20 @@ +#!/bin/sh +#MISE description="Run Playwright matrix tests (environment-aware)" +#MISE dir="packages/matrix" + +# Usage: mise run test-matrix [shard] +# In environment mode, uses Traefik-routed URLs; otherwise uses fixed ports. + +shard_flag=${1:+--shard} + +BASE_REALM_HOST="${REALM_BASE_URL:-http://localhost:4201}" +READY_PATH="_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson" +BASE_REALM_READY="http-get://${BASE_REALM_HOST#http://}/base/${READY_PATH}" + +echo "Waiting for base realm at ${BASE_REALM_HOST}..." +echo "Running matrix tests${1:+ (shard: $1)}" + +WAIT_ON_TIMEOUT=600000 start-server-and-test \ + 'pnpm run wait' \ + "$BASE_REALM_READY" \ + "pnpm playwright test ${shard_flag} ${1}" diff --git a/packages/matrix/docker/smtp4dev.ts b/packages/matrix/docker/smtp4dev.ts index 2da7690aec6..63b00e4485b 100644 --- a/packages/matrix/docker/smtp4dev.ts +++ b/packages/matrix/docker/smtp4dev.ts @@ -1,10 +1,25 @@ import { dockerCreateNetwork, dockerRun, dockerStop, dockerRm } from './index'; +import { + isEnvironmentMode, + getEnvironmentSlug, + registerServiceWithTraefik, + deregisterServiceFromTraefik, +} from '../helpers/environment-config'; +import { execSync } from 'child_process'; interface Options { mailClientPort?: number; } +function smtpContainerName(): string { + if (isEnvironmentMode()) { + return `boxel-smtp-${getEnvironmentSlug()}`; + } + return 'boxel-smtp'; +} + export async function smtpStart(opts?: Options) { + let containerName = smtpContainerName(); try { await smtpStop(); } catch (e: any) { @@ -12,22 +27,40 @@ export async function smtpStart(opts?: Options) { throw e; } } - let mailClientPort = opts?.mailClientPort ?? 5001; - let portMapping = `${mailClientPort}:80`; + let envMode = isEnvironmentMode(); + let mailClientPort = envMode + ? 0 + : (opts?.mailClientPort ?? parseInt(process.env.SMTP_PORT || '5001', 10)); + let portMapping = envMode ? '0:80' : `${mailClientPort}:80`; await dockerCreateNetwork({ networkName: 'boxel' }); const containerId = await dockerRun({ image: 'rnwood/smtp4dev:v3.1', - containerName: 'boxel-smtp', + containerName, dockerParams: ['-p', portMapping, '--network=boxel'], }); - console.log( - `Started smtp4dev with id ${containerId} mapped to host port ${mailClientPort}.`, - ); + if (envMode) { + let portOutput = execSync(`docker port ${containerId} 80/tcp`, { + encoding: 'utf-8', + }).trim(); + let hostPort = parseInt(portOutput.split('\n')[0].split(':').pop()!, 10); + registerServiceWithTraefik('smtp', hostPort); + console.log( + `Started smtp4dev with id ${containerId} on dynamic port ${hostPort} (Traefik).`, + ); + } else { + console.log( + `Started smtp4dev with id ${containerId} mapped to host port ${mailClientPort}.`, + ); + } return containerId; } export async function smtpStop() { - await dockerStop({ containerId: 'boxel-smtp' }); - await dockerRm({ containerId: 'boxel-smtp' }); + let containerName = smtpContainerName(); + if (isEnvironmentMode()) { + deregisterServiceFromTraefik('smtp'); + } + await dockerStop({ containerId: containerName }); + await dockerRm({ containerId: containerName }); } diff --git a/packages/matrix/helpers/environment-config.ts b/packages/matrix/helpers/environment-config.ts index dff1ad4f6e5..e843a0c4975 100644 --- a/packages/matrix/helpers/environment-config.ts +++ b/packages/matrix/helpers/environment-config.ts @@ -83,9 +83,11 @@ export function getSynapseURL(): string { } } -export function registerSynapseWithTraefik(hostPort: number): void { +export function registerServiceWithTraefik( + serviceName: string, + hostPort: number, +): void { let slug = getEnvironmentSlug(); - let serviceName = 'matrix'; let configPath = join(traefikDynamicDir(), `${slug}-${serviceName}.yml`); let routerKey = `${serviceName}-${slug}`; let hostname = `${serviceName}.${slug}.${DOMAIN}`; @@ -111,28 +113,38 @@ export function registerSynapseWithTraefik(hostPort: number): void { atomicWrite(configPath, yaml.stringify(config)); console.log( - `Registered Synapse at ${hostname} -> localhost:${hostPort}`, + `Registered ${serviceName} at ${hostname} -> localhost:${hostPort}`, ); } -export function deregisterSynapseFromTraefik(): void { +export function registerSynapseWithTraefik(hostPort: number): void { + registerServiceWithTraefik('matrix', hostPort); +} + +export function deregisterServiceFromTraefik(serviceName: string): void { if (!isEnvironmentMode()) { return; } let slug = getEnvironmentSlug(); - let configPath = join(traefikDynamicDir(), `${slug}-matrix.yml`); + let configPath = join(traefikDynamicDir(), `${slug}-${serviceName}.yml`); try { unlinkSync(configPath); - console.log(`Deregistered Synapse for environment ${slug} from Traefik`); + console.log( + `Deregistered ${serviceName} for environment ${slug} from Traefik`, + ); } catch (e: any) { if (e.code !== 'ENOENT') { console.error( - `Failed to deregister Synapse for environment ${slug}: ${e.message}`, + `Failed to deregister ${serviceName} for environment ${slug}: ${e.message}`, ); } } } +export function deregisterSynapseFromTraefik(): void { + deregisterServiceFromTraefik('matrix'); +} + function atomicWrite(filePath: string, content: string): void { let tmpPath = `${filePath}.tmp`; writeFileSync(tmpPath, content, 'utf-8'); diff --git a/packages/matrix/helpers/index.ts b/packages/matrix/helpers/index.ts index ec87953e035..944fc0c6c3e 100644 --- a/packages/matrix/helpers/index.ts +++ b/packages/matrix/helpers/index.ts @@ -12,12 +12,18 @@ import { } from '../docker/synapse'; import { realmPassword } from './realm-credentials'; import type { SQLExecutor } from './isolated-realm-server'; -import { appURL, BasicSQLExecutor } from './isolated-realm-server'; +import { + appURL, + serverIndexUrl, + realmDomain, + BasicSQLExecutor, +} from './isolated-realm-server'; import { APP_BOXEL_MESSAGE_MSGTYPE } from './matrix-constants'; import { randomUUID } from 'crypto'; -export const testHost = 'http://localhost:4205/test'; -export const mailHost = 'http://localhost:5001'; +export { realmDomain, serverIndexUrl }; +export const testHost = appURL; +export const mailHost = process.env.SMTP_URL || 'http://localhost:5001'; export const initialRoomName = 'New AI Assistant Chat'; export const REGISTRATION_TOKEN = 'abc123'; @@ -108,15 +114,16 @@ async function registerRealmRedirect( } export async function setRealmRedirects(page: Page) { + let baseServerUrl = process.env.REALM_BASE_URL || 'http://localhost:4201'; await registerRealmRedirect( page, - 'http://localhost:4201/skills/', - 'http://localhost:4205/skills/', + `${baseServerUrl}/skills/`, + `${serverIndexUrl}/skills/`, ); await registerRealmRedirect( page, - 'http://localhost:4201/base/', - 'http://localhost:4205/base/', + `${baseServerUrl}/base/`, + `${serverIndexUrl}/base/`, ); } diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index e4055dd79bf..e3d48fdb72c 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -6,6 +6,11 @@ import { ensureDirSync, copySync, readFileSync } from 'fs-extra'; import { Pool } from 'pg'; import { createServer as createNetServer, type AddressInfo } from 'net'; import type { SynapseInstance } from '../docker/synapse'; +import { + isEnvironmentMode, + registerServiceWithTraefik, + deregisterServiceFromTraefik, +} from './environment-config'; setGracefulCleanup(); @@ -18,7 +23,16 @@ const skillsRealmDir = resolve( ); const baseRealmDir = resolve(join(__dirname, '..', '..', 'base')); const matrixDir = resolve(join(__dirname, '..')); -export const appURL = 'http://localhost:4205/test'; + +const ISOLATED_REALM_SERVICE = 'realm-matrix-test'; +const ISOLATED_WORKER_SERVICE = 'worker-matrix-test'; + +// In environment mode, the isolated realm server is accessed via Traefik. +// The env var is set by mise-tasks/lib/env-vars.sh. +export const serverIndexUrl = + process.env.MATRIX_TEST_REALM_URL || 'http://localhost:4205'; +export const appURL = `${serverIndexUrl}/test`; +export const realmDomain = serverIndexUrl.replace(/^https?:\/\//, ''); const DEFAULT_PRERENDER_PORT = 4231; @@ -219,9 +233,20 @@ export async function startServer({ copySync(testRealmCards, testRealmDir); let testDBName = `test_db_${Math.floor(10000000 * Math.random())}`; - let workerManagerPort = await findAvailablePort(4232); - - process.env.PGPORT = '5435'; + let envMode = isEnvironmentMode(); + let preferredWorkerPort = envMode + ? 0 + : parseInt(process.env.MATRIX_TEST_WORKER_PORT || '4232', 10); + let workerManagerPort = await findAvailablePort(preferredWorkerPort); + let preferredRealmPort = envMode + ? 0 + : parseInt(process.env.MATRIX_TEST_REALM_PORT || '4205', 10); + let realmPort = await findAvailablePort(preferredRealmPort); + + let publishedDomain = + process.env.MATRIX_TEST_PUBLISHED_DOMAIN || realmDomain; + + process.env.PGPORT = process.env.PGPORT || '5435'; process.env.PGDATABASE = testDBName; process.env.NODE_NO_WARNINGS = '1'; process.env.REALM_SERVER_SECRET_SEED = "mum's the word"; @@ -241,12 +266,12 @@ export async function startServer({ `--prerendererUrl='${prerenderURL}'`, `--migrateDB`, - `--fromUrl='http://localhost:4205/test/'`, - `--toUrl='http://localhost:4205/test/'`, + `--fromUrl='${serverIndexUrl}/test/'`, + `--toUrl='${serverIndexUrl}/test/'`, ]; workerArgs = workerArgs.concat([ `--fromUrl='https://cardstack.com/base/'`, - `--toUrl='http://localhost:4205/base/'`, + `--toUrl='${serverIndexUrl}/base/'`, ]); let workerManager = spawn('ts-node', workerArgs, { @@ -267,7 +292,7 @@ export async function startServer({ let serverArgs = [ `--transpileOnly`, 'main', - `--port=4205`, + `--port=${realmPort}`, `--matrixURL='${matrixURL}'`, `--realmsRootPath='${dir.name}'`, `--workerManagerPort=${workerManagerPort}`, @@ -276,20 +301,20 @@ export async function startServer({ `--path='${testRealmDir}'`, `--username='test_realm'`, - `--fromUrl='http://localhost:4205/test/'`, - `--toUrl='http://localhost:4205/test/'`, + `--fromUrl='${serverIndexUrl}/test/'`, + `--toUrl='${serverIndexUrl}/test/'`, ]; serverArgs = serverArgs.concat([ `--username='skills_realm'`, `--path='${skillsRealmDir}'`, - `--fromUrl='http://localhost:4205/skills/'`, - `--toUrl='http://localhost:4205/skills/'`, + `--fromUrl='${serverIndexUrl}/skills/'`, + `--toUrl='${serverIndexUrl}/skills/'`, ]); serverArgs = serverArgs.concat([ `--username='base_realm'`, `--path='${baseRealmDir}'`, `--fromUrl='https://cardstack.com/base/'`, - `--toUrl='http://localhost:4205/base/'`, + `--toUrl='${serverIndexUrl}/base/'`, ]); console.log(`realm server database: ${testDBName}`); @@ -302,8 +327,8 @@ export async function startServer({ // Matrix tests don't exercise GitHub PR creation, so disable that route // to avoid pulling Octokit into the realm server startup path. DISABLE_GITHUB_PR_ROUTE: 'true', - PUBLISHED_REALM_BOXEL_SPACE_DOMAIN: 'localhost:4205', - PUBLISHED_REALM_BOXEL_SITE_DOMAIN: 'localhost:4205', + PUBLISHED_REALM_BOXEL_SPACE_DOMAIN: publishedDomain, + PUBLISHED_REALM_BOXEL_SITE_DOMAIN: publishedDomain, }, }); realmServer.unref(); @@ -349,11 +374,18 @@ export async function startServer({ ); } + // In environment mode, register the isolated realm server and worker with Traefik + if (envMode) { + registerServiceWithTraefik(ISOLATED_REALM_SERVICE, realmPort); + registerServiceWithTraefik(ISOLATED_WORKER_SERVICE, workerManagerPort); + } + return new IsolatedRealmServer( realmServer, workerManager, testRealmDir, testDBName, + envMode, ); } @@ -394,6 +426,7 @@ export class IsolatedRealmServer implements SQLExecutor { private workerManagerProcess: ReturnType, readonly realmPath: string, // useful for debugging readonly db: string, + private envMode: boolean = false, ) { workerManagerProcess.on('message', (message) => { if (message === 'stopped') { @@ -450,6 +483,11 @@ export class IsolatedRealmServer implements SQLExecutor { } async stop() { + if (this.envMode) { + deregisterServiceFromTraefik(ISOLATED_REALM_SERVICE); + deregisterServiceFromTraefik(ISOLATED_WORKER_SERVICE); + } + let realmServerStop = new Promise( (r) => (this.realmServerStopped = r), ); diff --git a/packages/matrix/playwright.config.ts b/packages/matrix/playwright.config.ts index adb45301874..70ad4290fba 100644 --- a/packages/matrix/playwright.config.ts +++ b/packages/matrix/playwright.config.ts @@ -1,9 +1,16 @@ import { defineConfig, devices } from '@playwright/test'; +import { appURL, realmDomain } from './helpers/isolated-realm-server'; /** * See https://playwright.dev/docs/test-configuration. */ +// In environment mode the isolated realm server is behind Traefik on port 80; +// in standard mode it listens on its own port (default 4205). +let resolverPort = realmDomain.includes(':') + ? realmDomain.split(':').pop()! + : '80'; + export default defineConfig({ testDir: './tests', fullyParallel: true, @@ -14,7 +21,7 @@ export default defineConfig({ reporter: process.env.CI ? 'blob' : 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - baseURL: 'http://localhost:4205/test', + baseURL: appURL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'retry-with-trace', @@ -35,7 +42,7 @@ export default defineConfig({ launchOptions: { args: [ // Simulate resolving a custom workspace domain to a realm server - '--host-resolver-rules=MAP published.realm 127.0.0.1:4205', + `--host-resolver-rules=MAP published.realm 127.0.0.1:${resolverPort}`, // Allow iframe to request storage access depsite being considered insecure '--unsafely-treat-insecure-origin-as-secure=http://published.realm', ], diff --git a/packages/matrix/scripts/test.sh b/packages/matrix/scripts/test.sh index 8eeda67de3a..58507a39f8f 100755 --- a/packages/matrix/scripts/test.sh +++ b/packages/matrix/scripts/test.sh @@ -2,7 +2,9 @@ shard_flag=${1:+--shard} echo "running tests: ${1}" -BASE_REALM="http-get://localhost:4201/base/" +BASE_REALM_HOST="${REALM_BASE_URL:-http://localhost:4201}" +# start-server-and-test needs http-get:// prefix (without the scheme from the URL) +BASE_REALM="http-get://${BASE_REALM_HOST#http://}/base/" READY_PATH="_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson" diff --git a/packages/matrix/tests/global.setup.ts b/packages/matrix/tests/global.setup.ts index b70ce27f5b1..7d49326fd08 100644 --- a/packages/matrix/tests/global.setup.ts +++ b/packages/matrix/tests/global.setup.ts @@ -11,6 +11,11 @@ import { import type { IsolatedRealmServer } from '../helpers/isolated-realm-server'; import { registerRealmUsers, REGISTRATION_TOKEN } from '../helpers'; import { smtpStart, smtpStop } from '../docker/smtp4dev'; +import { + isEnvironmentMode, + getSynapseURL, + deregisterSynapseFromTraefik, +} from '../helpers/environment-config'; export default async function setup() { await smtpStart(); @@ -19,7 +24,11 @@ export default async function setup() { let admin = await registerUser(synapse, 'admin', 'adminpass', true); await createRegistrationToken(admin.accessToken, REGISTRATION_TOKEN); const prerenderServer = await startPrerenderServer(); - const matrixURL = `http://localhost:${synapse.port}`; + // In environment mode the Synapse URL is routed through Traefik; + // otherwise use the direct localhost port. + const matrixURL = isEnvironmentMode() + ? getSynapseURL() + : `http://localhost:${synapse.port}`; let realmServer: IsolatedRealmServer; try { realmServer = await startRealmServer({ @@ -39,6 +48,7 @@ export default async function setup() { }); return async () => { await synapseStop(synapse.synapseId); + deregisterSynapseFromTraefik(); await realmServer.stop(); await prerenderServer.stop(); await smtpStop(); diff --git a/packages/matrix/tests/head-tags.spec.ts b/packages/matrix/tests/head-tags.spec.ts index 391e9537aab..c05bc0a1de1 100644 --- a/packages/matrix/tests/head-tags.spec.ts +++ b/packages/matrix/tests/head-tags.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from './fixtures'; import type { Page } from '@playwright/test'; import { randomUUID } from 'crypto'; -import { appURL } from '../helpers/isolated-realm-server'; +import { appURL, realmDomain } from '../helpers/isolated-realm-server'; import { clearLocalStorage, createRealm, @@ -82,7 +82,7 @@ test.describe('Head tags', () => { }) => { await publishDefaultRealm(page); - let publishedRealmURLString = `http://${user.username}.localhost:4205/new-workspace/index`; + let publishedRealmURLString = `http://${user.username}.${realmDomain}/new-workspace/index`; await page.goto(publishedRealmURLString); @@ -270,7 +270,7 @@ test.describe('Head tags', () => { await page.locator('[data-test-publish-button]').click(); await page.waitForSelector('[data-test-unpublish-button]'); - let publishedRealmURL = `http://${user.username}.localhost:4205/${realmName}/`; + let publishedRealmURL = `http://${user.username}.${realmDomain}/${realmName}/`; let defaultCardURL = `${publishedRealmURL}default-head-card.json`; await page.goto(defaultCardURL); diff --git a/packages/matrix/tests/host-mode.spec.ts b/packages/matrix/tests/host-mode.spec.ts index 00c5316a63c..e8311ef304e 100644 --- a/packages/matrix/tests/host-mode.spec.ts +++ b/packages/matrix/tests/host-mode.spec.ts @@ -6,7 +6,7 @@ import { postCardSource, waitUntil, } from '../helpers'; -import { appURL } from '../helpers/isolated-realm-server'; +import { appURL, serverIndexUrl, realmDomain } from '../helpers/isolated-realm-server'; import { randomUUID } from 'crypto'; test.describe('Host mode', () => { @@ -185,10 +185,10 @@ test.describe('Host mode', () => { await page.reload(); await page.locator('[data-test-host-mode-isolated]').waitFor(); - publishedRealmURL = `http://published.localhost:4205/${username}/${realmName}/`; + publishedRealmURL = `http://published.${realmDomain}/${username}/${realmName}/`; await page.evaluate( - async ({ realmURL, publishedRealmURL }) => { + async ({ realmURL, publishedRealmURL, realmServerUrl }) => { let sessions = JSON.parse( window.localStorage.getItem('boxel-session') ?? '{}', ); @@ -197,7 +197,7 @@ test.describe('Host mode', () => { throw new Error(`No session token found for ${realmURL}`); } - let response = await fetch('http://localhost:4205/_publish-realm', { + let response = await fetch(`${realmServerUrl}/_publish-realm`, { method: 'POST', headers: { Accept: 'application/json', @@ -216,13 +216,13 @@ test.describe('Host mode', () => { return response.json(); }, - { realmURL, publishedRealmURL }, + { realmURL, publishedRealmURL, realmServerUrl: serverIndexUrl }, ); publishedCardURL = `${publishedRealmURL}index.json`; publishedWhitePaperCardURL = `${publishedRealmURL}white-paper.json`; publishedMyCardURL = `${publishedRealmURL}my-card.json`; - connectRouteURL = `http://localhost:4205/connect/${encodeURIComponent( + connectRouteURL = `${serverIndexUrl}/connect/${encodeURIComponent( publishedRealmURL, )}`; @@ -322,7 +322,7 @@ test.describe('Host mode', () => { page, }) => { let response = await page.goto( - 'http://localhost:4205/connect/http%3A%2F%2Fexample.com', + `${serverIndexUrl}/connect/http%3A%2F%2Fexample.com`, ); expect(response?.status()).toBe(404); diff --git a/packages/matrix/tests/publish-realm.spec.ts b/packages/matrix/tests/publish-realm.spec.ts index 2d5e1eeed73..3e98a7b1ed3 100644 --- a/packages/matrix/tests/publish-realm.spec.ts +++ b/packages/matrix/tests/publish-realm.spec.ts @@ -1,6 +1,9 @@ import { test, expect } from './fixtures'; import type { Page } from '@playwright/test'; -import { appURL } from '../helpers/isolated-realm-server'; +import { + serverIndexUrl, + realmDomain, +} from '../helpers/isolated-realm-server'; import { clearLocalStorage, createRealm, @@ -9,8 +12,6 @@ import { postNewCard, } from '../helpers'; -let serverIndexUrl = new URL(appURL).origin; - test.describe('Publish realm', () => { let user: { username: string; password: string; credentials: any }; @@ -60,11 +61,11 @@ test.describe('Publish realm', () => { await newTab.waitForLoadState(); await expect(newTab).toHaveURL( - `http://${user.username}.localhost:4205/new-workspace/`, + `http://${user.username}.${realmDomain}/new-workspace/`, ); await expect( newTab.locator( - `[data-test-card="http://${user.username}.localhost:4205/new-workspace/index"]`, + `[data-test-card="http://${user.username}.${realmDomain}/new-workspace/index"]`, ), ).toBeVisible(); await newTab.close(); @@ -119,11 +120,11 @@ test.describe('Publish realm', () => { await newTab.waitForLoadState(); await expect(newTab).toHaveURL( - 'http://acceptable-subdomain.localhost:4205/', + `http://acceptable-subdomain.${realmDomain}/`, ); await expect( newTab.locator( - '[data-test-card="http://acceptable-subdomain.localhost:4205/index"]', + `[data-test-card="http://acceptable-subdomain.${realmDomain}/index"]`, ), ).toBeVisible(); await newTab.close(); @@ -251,7 +252,7 @@ test.describe('Publish realm', () => { await newTab.waitForLoadState(); await expect(newTab).toHaveURL( - `http://${user.username}.localhost:4205/new-workspace/`, + `http://${user.username}.${realmDomain}/new-workspace/`, ); await newTab.close(); await page.bringToFront(); @@ -281,7 +282,7 @@ test.describe('Publish realm', () => { await newTab.waitForLoadState(); await expect(newTab).toHaveURL( - `http://${user.username}.localhost:4205/new-workspace/`, + `http://${user.username}.${realmDomain}/new-workspace/`, ); await newTab.close(); await page.bringToFront(); diff --git a/packages/matrix/tests/registration-with-token.spec.ts b/packages/matrix/tests/registration-with-token.spec.ts index 498b60faa3c..66c4ffb6991 100644 --- a/packages/matrix/tests/registration-with-token.spec.ts +++ b/packages/matrix/tests/registration-with-token.spec.ts @@ -5,7 +5,7 @@ import { getAccountData, type SynapseInstance, } from '../docker/synapse'; -import { appURL } from '../helpers/isolated-realm-server'; +import { serverIndexUrl } from '../helpers/isolated-realm-server'; import { validateEmail, gotoRegistration, @@ -23,8 +23,6 @@ import { } from '../helpers'; import { APP_BOXEL_REALMS_EVENT_TYPE } from '../helpers/matrix-constants'; -const serverIndexUrl = new URL(appURL).origin; - function getSynapse(): SynapseInstance { return getMatrixTestContext().synapse; } @@ -253,7 +251,7 @@ test.describe('User Registration w/ Token', () => { APP_BOXEL_REALMS_EVENT_TYPE, ); expect(realms).toEqual({ - realms: [`http://localhost:4205/${firstUser.username}/personal/`], + realms: [`${serverIndexUrl}/${firstUser.username}/personal/`], }); }); diff --git a/packages/matrix/tests/skills.spec.ts b/packages/matrix/tests/skills.spec.ts index 629d521d222..f7c008596f5 100644 --- a/packages/matrix/tests/skills.spec.ts +++ b/packages/matrix/tests/skills.spec.ts @@ -15,7 +15,7 @@ import { createSubscribedUserAndLogin, createRealm, } from '../helpers'; -import { appURL } from '../helpers/isolated-realm-server'; +import { appURL, serverIndexUrl } from '../helpers/isolated-realm-server'; import { randomUUID } from 'crypto'; test.describe('Skills', () => { @@ -50,16 +50,14 @@ test.describe('Skills', () => { ).toContainClass('checked'); } - const environmentSkillCardId = `http://localhost:4205/skills/Skill/boxel-environment`; + const environmentSkillCardId = `${serverIndexUrl}/skills/Skill/boxel-environment`; const defaultSkillCardsForCodeMode = [ - `http://localhost:4205/skills/Skill/boxel-development`, + `${serverIndexUrl}/skills/Skill/boxel-development`, environmentSkillCardId, ]; const skillCard1 = `${appURL}/skill-pirate-speak`; const skillCard2 = `${appURL}/skill-seo`; const skillCard3 = `${appURL}/skill-card-title-editing`; - const serverIndexUrl = new URL(appURL).origin; - test(`it can attach skill cards and toggle activation`, async ({ page }) => { await login(page, firstUser.username, firstUser.password, { url: appURL }); await getRoomId(page); From a65ad04c6b31aac1b6a55d5c6b852e1bf2710f66 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Fri, 20 Mar 2026 18:01:25 -0700 Subject: [PATCH 02/11] Add prefix for start-server-and-test calls --- mise-tasks/test-matrix | 4 ++-- mise-tasks/test-services-matrix | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mise-tasks/test-matrix b/mise-tasks/test-matrix index 2c27464fd37..7ba01e7e106 100755 --- a/mise-tasks/test-matrix +++ b/mise-tasks/test-matrix @@ -14,7 +14,7 @@ BASE_REALM_READY="http-get://${BASE_REALM_HOST#http://}/base/${READY_PATH}" echo "Waiting for base realm at ${BASE_REALM_HOST}..." echo "Running matrix tests${1:+ (shard: $1)}" -WAIT_ON_TIMEOUT=600000 start-server-and-test \ +WAIT_ON_TIMEOUT=600000 pnpm exec start-server-and-test \ 'pnpm run wait' \ "$BASE_REALM_READY" \ - "pnpm playwright test ${shard_flag} ${1}" + "pnpm exec playwright test ${shard_flag} ${1}" diff --git a/mise-tasks/test-services-matrix b/mise-tasks/test-services-matrix index 3f8945eaa97..6533b67a9ea 100755 --- a/mise-tasks/test-services-matrix +++ b/mise-tasks/test-services-matrix @@ -8,7 +8,7 @@ READY_PATH="_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson" BASE_REALM_READY="http-get://${REALM_BASE_URL#http://}/base/${READY_PATH}" WAIT_ON_TIMEOUT=600000 NODE_NO_WARNINGS=1 SKIP_SUBMISSION=true \ - start-server-and-test \ - 'run-p -ln start:pg start:icons start:prerender-dev start:prerender-manager-dev start:worker-base start:base' \ + pnpm exec start-server-and-test \ + 'pnpm exec run-p -ln start:pg start:icons start:prerender-dev start:prerender-manager-dev start:worker-base start:base' \ "${BASE_REALM_READY}|${ICONS_URL}" \ 'wait' From b00ffc5d2b0342d75828aa26f9ae593686c36afb Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 09:00:10 -0700 Subject: [PATCH 03/11] Fix some services in environment mode --- packages/matrix/helpers/index.ts | 12 ++++++++++-- packages/matrix/helpers/isolated-realm-server.ts | 16 +++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/matrix/helpers/index.ts b/packages/matrix/helpers/index.ts index 944fc0c6c3e..18ed753cb27 100644 --- a/packages/matrix/helpers/index.ts +++ b/packages/matrix/helpers/index.ts @@ -18,12 +18,18 @@ import { realmDomain, BasicSQLExecutor, } from './isolated-realm-server'; +import { + isEnvironmentMode, + getEnvironmentSlug, +} from './environment-config'; import { APP_BOXEL_MESSAGE_MSGTYPE } from './matrix-constants'; import { randomUUID } from 'crypto'; export { realmDomain, serverIndexUrl }; export const testHost = appURL; -export const mailHost = process.env.SMTP_URL || 'http://localhost:5001'; +export const mailHost = isEnvironmentMode() + ? `http://smtp.${getEnvironmentSlug()}.localhost` + : 'http://localhost:5001'; export const initialRoomName = 'New AI Assistant Chat'; export const REGISTRATION_TOKEN = 'abc123'; @@ -114,7 +120,9 @@ async function registerRealmRedirect( } export async function setRealmRedirects(page: Page) { - let baseServerUrl = process.env.REALM_BASE_URL || 'http://localhost:4201'; + let baseServerUrl = isEnvironmentMode() + ? `http://realm-server.${getEnvironmentSlug()}.localhost` + : 'http://localhost:4201'; await registerRealmRedirect( page, `${baseServerUrl}/skills/`, diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index e3d48fdb72c..c0b6c2c90a5 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -8,6 +8,7 @@ import { createServer as createNetServer, type AddressInfo } from 'net'; import type { SynapseInstance } from '../docker/synapse'; import { isEnvironmentMode, + getEnvironmentSlug, registerServiceWithTraefik, deregisterServiceFromTraefik, } from './environment-config'; @@ -27,10 +28,13 @@ const matrixDir = resolve(join(__dirname, '..')); const ISOLATED_REALM_SERVICE = 'realm-matrix-test'; const ISOLATED_WORKER_SERVICE = 'worker-matrix-test'; -// In environment mode, the isolated realm server is accessed via Traefik. -// The env var is set by mise-tasks/lib/env-vars.sh. -export const serverIndexUrl = - process.env.MATRIX_TEST_REALM_URL || 'http://localhost:4205'; +// Compute URLs from BOXEL_ENVIRONMENT directly so that setting just that +// one env var is sufficient — no need to source env-vars.sh first. +const envMode = isEnvironmentMode(); +const envSlug = envMode ? getEnvironmentSlug() : ''; +export const serverIndexUrl = envMode + ? `http://${ISOLATED_REALM_SERVICE}.${envSlug}.localhost` + : 'http://localhost:4205'; export const appURL = `${serverIndexUrl}/test`; export const realmDomain = serverIndexUrl.replace(/^https?:\/\//, ''); @@ -157,7 +161,9 @@ export async function startPrerenderServer( ...process.env, NODE_ENV: process.env.NODE_ENV ?? 'development', NODE_NO_WARNINGS: '1', - BOXEL_HOST_URL: process.env.HOST_URL ?? 'http://localhost:4200', + BOXEL_HOST_URL: envMode + ? `http://host.${envSlug}.localhost` + : (process.env.HOST_URL ?? 'http://localhost:4200'), LOG_LEVELS: process.env.SOFTWARE_FACTORY_PRERENDER_LOG_LEVELS ?? process.env.LOG_LEVELS, }; From 8580d46c5e68b51398c42e1d107d7320ff6568df Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 09:02:29 -0700 Subject: [PATCH 04/11] Add more domain conditionals --- mise-tasks/services/realm-server-base | 16 +++++++++++----- mise-tasks/services/worker-base | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/mise-tasks/services/realm-server-base b/mise-tasks/services/realm-server-base index 63541c4459d..f99b5be6ead 100755 --- a/mise-tasks/services/realm-server-base +++ b/mise-tasks/services/realm-server-base @@ -8,10 +8,16 @@ if [ -z "$MATRIX_REGISTRATION_SHARED_SECRET" ]; then export MATRIX_REGISTRATION_SHARED_SECRET fi +if [ -n "$ENV_MODE" ]; then + WORKER_MANAGER_ARG="--workerManagerUrl=${WORKER_MGR_URL}" +else + WORKER_MANAGER_ARG="$1" +fi + NODE_ENV=development \ NODE_NO_WARNINGS=1 \ PGPORT="${PGPORT}" \ - PGDATABASE=boxel_base \ + PGDATABASE="${PGDATABASE:-boxel_base}" \ REALM_SERVER_SECRET_SEED="mum's the word" \ REALM_SECRET_SEED="shhh! it's a secret" \ GRAFANA_SECRET="shhh! it's a secret" \ @@ -19,14 +25,14 @@ NODE_ENV=development \ REALM_SERVER_MATRIX_USERNAME=realm_server \ ts-node \ --transpileOnly main \ - --port=4201 \ + --port="${REALM_PORT:-4201}" \ --matrixURL="${MATRIX_URL_VAL}" \ - --realmsRootPath='./realms/localhost_4201_base' \ + --realmsRootPath="${REALMS_ROOT:-./realms/localhost_4201_base}" \ --prerendererUrl="${PRERENDER_URL}" \ --migrateDB \ - $1 \ + $WORKER_MANAGER_ARG \ \ --path='../base' \ --username='base_realm' \ --fromUrl='https://cardstack.com/base/' \ - --toUrl='http://localhost:4201/base/' + --toUrl="${REALM_BASE_URL:-http://localhost:4201}/base/" diff --git a/mise-tasks/services/worker-base b/mise-tasks/services/worker-base index 0f6aa184d66..4d180a0f4ca 100755 --- a/mise-tasks/services/worker-base +++ b/mise-tasks/services/worker-base @@ -7,15 +7,15 @@ NODE_ENV=development \ NODE_NO_WARNINGS=1 \ NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=4096}" \ PGPORT="${PGPORT}" \ - PGDATABASE=boxel_base \ + PGDATABASE="${PGDATABASE:-boxel_base}" \ REALM_SECRET_SEED="shhh! it's a secret" \ REALM_SERVER_MATRIX_USERNAME=realm_server \ LOW_CREDIT_THRESHOLD=2000 \ ts-node \ --transpileOnly worker-manager \ - --port=4213 \ + --port="${WORKER_PORT:-4213}" \ --matrixURL="${MATRIX_URL_VAL}" \ --prerendererUrl="${PRERENDER_MGR_URL}" \ \ --fromUrl='https://cardstack.com/base/' \ - --toUrl='http://localhost:4201/base/' + --toUrl="${REALM_BASE_URL:-http://localhost:4201}/base/" From 08243a14ce1b792b8c23c2619db15f3a4cf3d0cc Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 09:13:36 -0700 Subject: [PATCH 05/11] Fix more URLs and ports --- packages/matrix/helpers/isolated-realm-server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index c0b6c2c90a5..f09fec333fe 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -71,7 +71,8 @@ async function isPortAvailable(port: number): Promise { } async function findAvailablePort(preferred?: number): Promise { - if (typeof preferred === 'number' && (await isPortAvailable(preferred))) { + // port 0 means "pick any available port" — skip straight to dynamic allocation + if (typeof preferred === 'number' && preferred > 0 && (await isPortAvailable(preferred))) { return preferred; } return await new Promise((resolve, reject) => { @@ -258,7 +259,9 @@ export async function startServer({ process.env.REALM_SERVER_SECRET_SEED = "mum's the word"; process.env.REALM_SECRET_SEED = "shhh! it's a secret"; process.env.GRAFANA_SECRET = "shhh! it's a secret"; - let matrixURL = `http://localhost:${synapse.port}`; + let matrixURL = envMode + ? `http://matrix.${envSlug}.localhost` + : `http://localhost:${synapse.port}`; process.env.MATRIX_URL = matrixURL; process.env.REALM_SERVER_MATRIX_USERNAME = 'realm_server'; process.env.NODE_ENV = 'test'; From fcae21be828024d5cb627fe9afb5acbbf7bb3310 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 09:17:47 -0700 Subject: [PATCH 06/11] Change prerenderer to be isolated --- .../matrix/helpers/isolated-realm-server.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index f09fec333fe..b7d4c542edc 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -27,6 +27,7 @@ const matrixDir = resolve(join(__dirname, '..')); const ISOLATED_REALM_SERVICE = 'realm-matrix-test'; const ISOLATED_WORKER_SERVICE = 'worker-matrix-test'; +const ISOLATED_PRERENDER_SERVICE = 'prerender-matrix-test'; // Compute URLs from BOXEL_ENVIRONMENT directly so that setting just that // one env var is sufficient — no need to source env-vars.sh first. @@ -155,8 +156,9 @@ function stopChildProcess( export async function startPrerenderServer( options?: PrerenderServerConfig, ): Promise { - let port = await findAvailablePort(options?.port ?? DEFAULT_PRERENDER_PORT); - let url = `http://localhost:${port}`; + let preferredPort = envMode ? 0 : (options?.port ?? DEFAULT_PRERENDER_PORT); + let port = await findAvailablePort(preferredPort); + let localUrl = `http://localhost:${port}`; let silent = process.env.SOFTWARE_FACTORY_PRERENDER_SILENT !== '0'; let env = { ...process.env, @@ -211,7 +213,7 @@ export async function startPrerenderServer( }); try { - await Promise.race([waitForHttpReady(url, 60_000), exitPromise]); + await Promise.race([waitForHttpReady(localUrl, 60_000), exitPromise]); } finally { if (exitListener) { child.removeListener('exit', exitListener); @@ -221,10 +223,22 @@ export async function startPrerenderServer( } } + // In env mode, register with Traefik so parallel environments don't collide + let url: string; + if (envMode) { + registerServiceWithTraefik(ISOLATED_PRERENDER_SERVICE, port); + url = `http://${ISOLATED_PRERENDER_SERVICE}.${envSlug}.localhost`; + } else { + url = localUrl; + } + return { port, url, async stop() { + if (envMode) { + deregisterServiceFromTraefik(ISOLATED_PRERENDER_SERVICE); + } await stopChildProcess(child); }, }; From dec1d46c80fd49e4fcdce9ec9f4fe842d8cdc90b Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 09:41:36 -0700 Subject: [PATCH 07/11] Fix registration order --- packages/matrix/helpers/isolated-realm-server.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index b7d4c542edc..96767ef92da 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -267,6 +267,13 @@ export async function startServer({ let publishedDomain = process.env.MATRIX_TEST_PUBLISHED_DOMAIN || realmDomain; + // Register with Traefik BEFORE spawning processes so the worker can + // reach the realm server via the Traefik hostname from the start. + if (envMode) { + registerServiceWithTraefik(ISOLATED_REALM_SERVICE, realmPort); + registerServiceWithTraefik(ISOLATED_WORKER_SERVICE, workerManagerPort); + } + process.env.PGPORT = process.env.PGPORT || '5435'; process.env.PGDATABASE = testDBName; process.env.NODE_NO_WARNINGS = '1'; @@ -397,12 +404,6 @@ export async function startServer({ ); } - // In environment mode, register the isolated realm server and worker with Traefik - if (envMode) { - registerServiceWithTraefik(ISOLATED_REALM_SERVICE, realmPort); - registerServiceWithTraefik(ISOLATED_WORKER_SERVICE, workerManagerPort); - } - return new IsolatedRealmServer( realmServer, workerManager, From 79dcb209f22044221f5960b61d21c73707ad0ca2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 10:21:49 -0700 Subject: [PATCH 08/11] Fix prerender URL in environment mode --- packages/matrix/helpers/isolated-realm-server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index 96767ef92da..fdc8aea0d1a 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -164,8 +164,11 @@ export async function startPrerenderServer( ...process.env, NODE_ENV: process.env.NODE_ENV ?? 'development', NODE_NO_WARNINGS: '1', + // Point the prerender at the isolated realm server itself — it proxies + // host app assets via distURL and serves realm content at the correct + // URLs. Standby creation will retry until the realm server is up. BOXEL_HOST_URL: envMode - ? `http://host.${envSlug}.localhost` + ? serverIndexUrl : (process.env.HOST_URL ?? 'http://localhost:4200'), LOG_LEVELS: process.env.SOFTWARE_FACTORY_PRERENDER_LOG_LEVELS ?? process.env.LOG_LEVELS, @@ -326,7 +329,7 @@ export async function startServer({ `--matrixURL='${matrixURL}'`, `--realmsRootPath='${dir.name}'`, `--workerManagerPort=${workerManagerPort}`, - `--prerendererUrl="${prerenderURL}"`, + `--prerendererUrl='${prerenderURL}'`, `--useRegistrationSecretFunction`, `--path='${testRealmDir}'`, From cfde967e932425b7a8be631cad771aa968e23ccb Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 10:34:21 -0700 Subject: [PATCH 09/11] Add Traefik-awareness for some startup scripts --- mise-tasks/services/realm-server-base | 2 +- mise-tasks/services/worker-base | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mise-tasks/services/realm-server-base b/mise-tasks/services/realm-server-base index f99b5be6ead..b6a80d07274 100755 --- a/mise-tasks/services/realm-server-base +++ b/mise-tasks/services/realm-server-base @@ -1,6 +1,6 @@ #!/bin/sh #MISE description="Start base realm server only" -#MISE depends=["infra:ensure-pg"] +#MISE depends=["infra:ensure-traefik", "infra:ensure-pg"] #MISE dir="packages/realm-server" if [ -z "$MATRIX_REGISTRATION_SHARED_SECRET" ]; then diff --git a/mise-tasks/services/worker-base b/mise-tasks/services/worker-base index 4d180a0f4ca..2523ba74561 100755 --- a/mise-tasks/services/worker-base +++ b/mise-tasks/services/worker-base @@ -1,6 +1,6 @@ #!/bin/sh #MISE description="Start worker manager for base realm only" -#MISE depends=["infra:ensure-pg", "infra:wait-for-prerender"] +#MISE depends=["infra:ensure-traefik", "infra:ensure-pg", "infra:wait-for-prerender"] #MISE dir="packages/realm-server" NODE_ENV=development \ From fdc350434254d368a409511005adcc4842ca045e Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 11:33:52 -0700 Subject: [PATCH 10/11] Add paralell infrastructure for Matrix tests --- packages/matrix/docker/smtp4dev.ts | 12 +++++-- packages/matrix/docker/synapse/index.ts | 6 ++-- packages/matrix/helpers/environment-config.ts | 9 ++++++ packages/matrix/helpers/index.ts | 2 +- .../matrix/helpers/isolated-realm-server.ts | 8 ++++- packages/matrix/tests/global.setup.ts | 31 +++++++++++++------ .../prerender/prerender-server.ts | 5 ++- 7 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/matrix/docker/smtp4dev.ts b/packages/matrix/docker/smtp4dev.ts index 63b00e4485b..0e30d7f91fd 100644 --- a/packages/matrix/docker/smtp4dev.ts +++ b/packages/matrix/docker/smtp4dev.ts @@ -9,16 +9,22 @@ import { execSync } from 'child_process'; interface Options { mailClientPort?: number; + traefikServiceName?: string; } +let _smtpServiceName = 'smtp'; + function smtpContainerName(): string { if (isEnvironmentMode()) { - return `boxel-smtp-${getEnvironmentSlug()}`; + return `boxel-${_smtpServiceName}-${getEnvironmentSlug()}`; } return 'boxel-smtp'; } export async function smtpStart(opts?: Options) { + if (opts?.traefikServiceName) { + _smtpServiceName = opts.traefikServiceName; + } let containerName = smtpContainerName(); try { await smtpStop(); @@ -44,7 +50,7 @@ export async function smtpStart(opts?: Options) { encoding: 'utf-8', }).trim(); let hostPort = parseInt(portOutput.split('\n')[0].split(':').pop()!, 10); - registerServiceWithTraefik('smtp', hostPort); + registerServiceWithTraefik(_smtpServiceName, hostPort); console.log( `Started smtp4dev with id ${containerId} on dynamic port ${hostPort} (Traefik).`, ); @@ -59,7 +65,7 @@ export async function smtpStart(opts?: Options) { export async function smtpStop() { let containerName = smtpContainerName(); if (isEnvironmentMode()) { - deregisterServiceFromTraefik('smtp'); + deregisterServiceFromTraefik(_smtpServiceName); } await dockerStop({ containerId: containerName }); await dockerRm({ containerId: containerName }); diff --git a/packages/matrix/docker/synapse/index.ts b/packages/matrix/docker/synapse/index.ts index 095de13bb64..3d6fd662c64 100644 --- a/packages/matrix/docker/synapse/index.ts +++ b/packages/matrix/docker/synapse/index.ts @@ -16,7 +16,7 @@ import { isEnvironmentMode, getSynapseContainerName, getSynapseURL, - registerSynapseWithTraefik, + registerServiceWithTraefik, } from '../../helpers/environment-config'; export const SYNAPSE_IP_ADDRESS = '172.20.0.5'; @@ -110,6 +110,7 @@ interface StartOptions { dataDir?: string; containerName?: string; suppressRegistrationSecretFile?: true; + traefikServiceName?: string; } export async function synapseStart( opts?: StartOptions, @@ -194,7 +195,8 @@ export async function synapseStart( let firstLine = portOutput.split('\n')[0]; let hostPort = parseInt(firstLine.split(':').pop()!, 10); console.log(`Synapse dynamic host port: ${hostPort}`); - registerSynapseWithTraefik(hostPort); + let synapseServiceName = opts?.traefikServiceName || 'matrix'; + registerServiceWithTraefik(synapseServiceName, hostPort); } const synapse: SynapseInstance = { synapseId, ...synCfg }; diff --git a/packages/matrix/helpers/environment-config.ts b/packages/matrix/helpers/environment-config.ts index e843a0c4975..15622c6fcbe 100644 --- a/packages/matrix/helpers/environment-config.ts +++ b/packages/matrix/helpers/environment-config.ts @@ -64,7 +64,16 @@ export function getSynapseContainerName(): string { return 'boxel-synapse'; } +let _synapseURLOverride: string | undefined; + +export function setSynapseURL(url: string): void { + _synapseURLOverride = url; +} + export function getSynapseURL(): string { + if (_synapseURLOverride) { + return _synapseURLOverride; + } if (!isEnvironmentMode()) { return 'http://localhost:8008'; } diff --git a/packages/matrix/helpers/index.ts b/packages/matrix/helpers/index.ts index 18ed753cb27..eedca174bf3 100644 --- a/packages/matrix/helpers/index.ts +++ b/packages/matrix/helpers/index.ts @@ -28,7 +28,7 @@ import { randomUUID } from 'crypto'; export { realmDomain, serverIndexUrl }; export const testHost = appURL; export const mailHost = isEnvironmentMode() - ? `http://smtp.${getEnvironmentSlug()}.localhost` + ? `http://smtp-test.${getEnvironmentSlug()}.localhost` : 'http://localhost:5001'; export const initialRoomName = 'New AI Assistant Chat'; export const REGISTRATION_TOKEN = 'abc123'; diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index fdc8aea0d1a..8f25a523abb 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -170,6 +170,8 @@ export async function startPrerenderServer( BOXEL_HOST_URL: envMode ? serverIndexUrl : (process.env.HOST_URL ?? 'http://localhost:4200'), + // Use a distinct service name so it doesn't overwrite the dev prerender + PRERENDER_SERVICE_NAME: ISOLATED_PRERENDER_SERVICE, LOG_LEVELS: process.env.SOFTWARE_FACTORY_PRERENDER_LOG_LEVELS ?? process.env.LOG_LEVELS, }; @@ -284,7 +286,7 @@ export async function startServer({ process.env.REALM_SECRET_SEED = "shhh! it's a secret"; process.env.GRAFANA_SECRET = "shhh! it's a secret"; let matrixURL = envMode - ? `http://matrix.${envSlug}.localhost` + ? `http://matrix-test.${envSlug}.localhost` : `http://localhost:${synapse.port}`; process.env.MATRIX_URL = matrixURL; process.env.REALM_SERVER_MATRIX_USERNAME = 'realm_server'; @@ -298,6 +300,8 @@ export async function startServer({ `--matrixURL='${matrixURL}'`, `--prerendererUrl='${prerenderURL}'`, `--migrateDB`, + // Use a distinct service name so the worker doesn't overwrite the dev worker + ...(envMode ? [`--serviceName='${ISOLATED_WORKER_SERVICE}'`] : []), `--fromUrl='${serverIndexUrl}/test/'`, `--toUrl='${serverIndexUrl}/test/'`, @@ -331,6 +335,8 @@ export async function startServer({ `--workerManagerPort=${workerManagerPort}`, `--prerendererUrl='${prerenderURL}'`, `--useRegistrationSecretFunction`, + // Use a distinct service name so it doesn't overwrite the dev realm server + ...(envMode ? [`--serviceName='${ISOLATED_REALM_SERVICE}'`] : []), `--path='${testRealmDir}'`, `--username='test_realm'`, diff --git a/packages/matrix/tests/global.setup.ts b/packages/matrix/tests/global.setup.ts index 7d49326fd08..666c045bed2 100644 --- a/packages/matrix/tests/global.setup.ts +++ b/packages/matrix/tests/global.setup.ts @@ -13,22 +13,33 @@ import { registerRealmUsers, REGISTRATION_TOKEN } from '../helpers'; import { smtpStart, smtpStop } from '../docker/smtp4dev'; import { isEnvironmentMode, - getSynapseURL, - deregisterSynapseFromTraefik, + getEnvironmentSlug, + deregisterServiceFromTraefik, + setSynapseURL, } from '../helpers/environment-config'; +// Distinct service names so matrix tests don't overwrite dev services +const MATRIX_TEST_SYNAPSE_SERVICE = 'matrix-test'; +const MATRIX_TEST_SMTP_SERVICE = 'smtp-test'; + export default async function setup() { - await smtpStart(); - const synapse = await synapseStart(); + await smtpStart({ traefikServiceName: MATRIX_TEST_SMTP_SERVICE }); + const synapse = await synapseStart({ + traefikServiceName: MATRIX_TEST_SYNAPSE_SERVICE, + }); await registerRealmUsers(synapse); let admin = await registerUser(synapse, 'admin', 'adminpass', true); await createRegistrationToken(admin.accessToken, REGISTRATION_TOKEN); + const prerenderServer = await startPrerenderServer(); - // In environment mode the Synapse URL is routed through Traefik; - // otherwise use the direct localhost port. - const matrixURL = isEnvironmentMode() - ? getSynapseURL() + // In environment mode the Synapse URL is routed through Traefik + // using a test-specific service name; otherwise use the direct localhost port. + const envMode = isEnvironmentMode(); + const matrixURL = envMode + ? `http://${MATRIX_TEST_SYNAPSE_SERVICE}.${getEnvironmentSlug()}.localhost` : `http://localhost:${synapse.port}`; + // Override so all Synapse API calls in synapse/index.ts use the test instance + setSynapseURL(matrixURL); let realmServer: IsolatedRealmServer; try { realmServer = await startRealmServer({ @@ -48,7 +59,9 @@ export default async function setup() { }); return async () => { await synapseStop(synapse.synapseId); - deregisterSynapseFromTraefik(); + if (envMode) { + deregisterServiceFromTraefik(MATRIX_TEST_SYNAPSE_SERVICE); + } await realmServer.stop(); await prerenderServer.stop(); await smtpStop(); diff --git a/packages/realm-server/prerender/prerender-server.ts b/packages/realm-server/prerender/prerender-server.ts index 2e8a762d982..523d2fba014 100644 --- a/packages/realm-server/prerender/prerender-server.ts +++ b/packages/realm-server/prerender/prerender-server.ts @@ -37,7 +37,10 @@ webServerInstance.on('listening', () => { actualPort = (webServerInstance!.address() as import('net').AddressInfo).port ?? port; if (isEnvironmentMode()) { - registerService(webServerInstance!, 'prerender'); + registerService( + webServerInstance!, + process.env.PRERENDER_SERVICE_NAME || 'prerender', + ); } log.info(`prerender server HTTP listening on port ${actualPort}`); }); From 62e2909762a02b926ef67dc1fdb211673060a436 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Mon, 23 Mar 2026 13:29:14 -0700 Subject: [PATCH 11/11] Change prerender server in environment mode --- packages/matrix/helpers/isolated-realm-server.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/matrix/helpers/isolated-realm-server.ts b/packages/matrix/helpers/isolated-realm-server.ts index 8f25a523abb..e0705132e67 100644 --- a/packages/matrix/helpers/isolated-realm-server.ts +++ b/packages/matrix/helpers/isolated-realm-server.ts @@ -164,11 +164,11 @@ export async function startPrerenderServer( ...process.env, NODE_ENV: process.env.NODE_ENV ?? 'development', NODE_NO_WARNINGS: '1', - // Point the prerender at the isolated realm server itself — it proxies - // host app assets via distURL and serves realm content at the correct - // URLs. Standby creation will retry until the realm server is up. + // Point the prerender at the host app directly (not the isolated realm + // server) to avoid a deadlock: standby creation needs the host app shell, + // and the realm server's indexing needs the prerender to be ready. BOXEL_HOST_URL: envMode - ? serverIndexUrl + ? `http://host.${envSlug}.localhost` : (process.env.HOST_URL ?? 'http://localhost:4200'), // Use a distinct service name so it doesn't overwrite the dev prerender PRERENDER_SERVICE_NAME: ISOLATED_PRERENDER_SERVICE,