Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fe0ed0c
Add preliminary environment mode for Matrix tests
backspace Mar 21, 2026
a65ad04
Add prefix for start-server-and-test calls
backspace Mar 21, 2026
b00ffc5
Fix some services in environment mode
backspace Mar 23, 2026
8580d46
Add more domain conditionals
backspace Mar 23, 2026
08243a1
Fix more URLs and ports
backspace Mar 23, 2026
fcae21b
Change prerenderer to be isolated
backspace Mar 23, 2026
dec1d46
Fix registration order
backspace Mar 23, 2026
79dcb20
Fix prerender URL in environment mode
backspace Mar 23, 2026
cfde967
Add Traefik-awareness for some startup scripts
backspace Mar 23, 2026
fdc3504
Add paralell infrastructure for Matrix tests
backspace Mar 23, 2026
62e2909
Change prerender server in environment mode
backspace Mar 23, 2026
068fdd2
Add more environment mode fixes
backspace Mar 26, 2026
7131eb2
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Mar 26, 2026
460b438
Fix more setup
backspace Mar 26, 2026
7edddb6
Change isolated realm server resolution
backspace Mar 26, 2026
f6758de
Add missing PATH setup
backspace Mar 26, 2026
5b94b32
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Mar 26, 2026
e8927aa
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Mar 26, 2026
8c5dba9
Add pooling maximum for isolated realm server
backspace Mar 26, 2026
128f720
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Mar 26, 2026
ff0be17
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Mar 27, 2026
bdcdcf3
matrix: Environment-mode support for Matrix Playwright tests
backspace Apr 1, 2026
91275af
matrix: Use separate test Synapse domain via realm server config rewrite
backspace Apr 1, 2026
45fbfde
Merge remote-tracking branch 'origin/main' into matrix/tests-environm…
backspace Apr 2, 2026
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
3 changes: 3 additions & 0 deletions mise-tasks/dev
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#MISE description="Start full dev stack (realm server, workers, test realms)"
#MISE dir="packages/realm-server"

# Add node_modules/.bin to PATH (mise run bypasses pnpm which normally does this)
PATH="$(pwd)/../../node_modules/.bin:$(pwd)/node_modules/.bin:$PATH"

. "$(cd "$(dirname "$0")" && pwd)/lib/dev-common.sh"

WAIT_ON_TIMEOUT=7200000 NODE_NO_WARNINGS=1 start-server-and-test \
Expand Down
24 changes: 24 additions & 0 deletions mise-tasks/lib/env-vars.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions mise-tasks/services/realm-server-base
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
#!/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
MATRIX_REGISTRATION_SHARED_SECRET=$(ts-node --transpileOnly ./scripts/matrix-registration-secret.ts)
export MATRIX_REGISTRATION_SHARED_SECRET
fi

# The base-only realm server uses dedicated port/db/paths that differ from
# the main development realm server. In env mode, use the env vars; in
# standard mode, use the base-specific defaults.
if [ -n "$ENV_MODE" ]; then
WORKER_MANAGER_ARG="--workerManagerUrl=${WORKER_MGR_URL}"
REALM_BASE_PORT="${REALM_PORT}"
REALM_BASE_DB="${PGDATABASE}"
REALM_BASE_ROOT="${REALMS_ROOT}"
REALM_BASE_TO_URL="${REALM_BASE_URL}/base/"
else
WORKER_MANAGER_ARG="$1"
REALM_BASE_PORT=4201
REALM_BASE_DB=boxel_base
REALM_BASE_ROOT="./realms/localhost_4201_base"
REALM_BASE_TO_URL="http://localhost:4201/base/"
fi

NODE_ENV=development \
NODE_NO_WARNINGS=1 \
PGPORT="${PGPORT}" \
PGDATABASE=boxel_base \
PGDATABASE="${REALM_BASE_DB}" \
REALM_SERVER_SECRET_SEED="mum's the word" \
REALM_SECRET_SEED="shhh! it's a secret" \
GRAFANA_SECRET="shhh! it's a secret" \
MATRIX_URL="${MATRIX_URL_VAL}" \
REALM_SERVER_MATRIX_USERNAME=realm_server \
ts-node \
--transpileOnly main \
--port=4201 \
--port="${REALM_BASE_PORT}" \
--matrixURL="${MATRIX_URL_VAL}" \
--realmsRootPath='./realms/localhost_4201_base' \
--realmsRootPath="${REALM_BASE_ROOT}" \
--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_TO_URL}"
19 changes: 15 additions & 4 deletions mise-tasks/services/worker-base
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
#!/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"

# The base-only worker uses dedicated port/db that differ from the main
# development worker (WORKER_PORT/PGDATABASE). In env mode, use the env
# vars; in standard mode, use the base-specific defaults.
if [ -n "$ENV_MODE" ]; then
WORKER_BASE_PORT="${WORKER_PORT}"
WORKER_BASE_DB="${PGDATABASE}"
else
WORKER_BASE_PORT=4213
WORKER_BASE_DB=boxel_base
fi

NODE_ENV=development \
NODE_NO_WARNINGS=1 \
NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=4096}" \
PGPORT="${PGPORT}" \
PGDATABASE=boxel_base \
PGDATABASE="${WORKER_BASE_DB}" \
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_BASE_PORT}" \
--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/"
20 changes: 20 additions & 0 deletions mise-tasks/test-matrix
Original file line number Diff line number Diff line change
@@ -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 pnpm exec start-server-and-test \
'pnpm run wait' \
"$BASE_REALM_READY" \
"pnpm exec playwright test ${shard_flag} ${1}"
55 changes: 47 additions & 8 deletions packages/matrix/docker/smtp4dev.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,72 @@
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;
traefikServiceName?: string;
}

let _smtpServiceName = 'smtp';

function smtpContainerName(): string {
if (isEnvironmentMode()) {
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();
} catch (e: any) {
if (!e.message.includes('No such container')) {
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(_smtpServiceName, 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(_smtpServiceName);
}
await dockerStop({ containerId: containerName });
await dockerRm({ containerId: containerName });
}
9 changes: 7 additions & 2 deletions packages/matrix/docker/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
isEnvironmentMode,
getSynapseContainerName,
getSynapseURL,
registerSynapseWithTraefik,
registerServiceWithTraefik,
} from '../../helpers/environment-config';

export const SYNAPSE_IP_ADDRESS = '172.20.0.5';
Expand Down Expand Up @@ -157,6 +157,7 @@ interface StartOptions {
dataDir?: string;
containerName?: string;
suppressRegistrationSecretFile?: true;
traefikServiceName?: string;
dynamicHostPort?: true;
}

Expand Down Expand Up @@ -230,6 +231,9 @@ export async function synapseStart(
);
}

// Clean up stale container from a previous interrupted run
await dockerStop({ containerId: containerName }).catch(() => {});

try {
synapseId = await dockerRun({
image: 'matrixdotorg/synapse:v1.126.0',
Expand Down Expand Up @@ -286,7 +290,8 @@ export async function synapseStart(
}

if (isEnvironmentMode()) {
registerSynapseWithTraefik(hostPort);
let synapseServiceName = opts?.traefikServiceName || 'matrix';
registerServiceWithTraefik(synapseServiceName, hostPort);
}

const synapse: SynapseInstance = {
Expand Down
50 changes: 43 additions & 7 deletions packages/matrix/helpers/environment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,32 @@ export function getSynapseContainerName(): string {
return 'boxel-synapse';
}

let _synapseURLOverride: string | undefined;

export function setSynapseURL(url: string): void {
_synapseURLOverride = url;
}

export function getSynapseURL(synapse?: {
baseUrl?: string;
port?: number;
}): string {
if (_synapseURLOverride) {
return _synapseURLOverride;
}
// In Playwright worker processes, _synapseURLOverride isn't set (it was set
// in the global.setup process). Fall back to MATRIX_TEST_CONTEXT which IS
// shared via env var.
if (process.env.MATRIX_TEST_CONTEXT) {
try {
let ctx = JSON.parse(process.env.MATRIX_TEST_CONTEXT);
if (ctx.matrixUrl) {
return ctx.matrixUrl;
}
} catch {
// ignore parse errors
}
}
if (synapse?.baseUrl) {
return synapse.baseUrl;
}
Expand All @@ -88,9 +110,11 @@ export function getSynapseURL(synapse?: {
}
}

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}`;
Expand All @@ -115,27 +139,39 @@ export function registerSynapseWithTraefik(hostPort: number): void {
};

atomicWrite(configPath, yaml.stringify(config));
console.log(`Registered Synapse at ${hostname} -> localhost:${hostPort}`);
console.log(
`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');
Expand Down
Loading
Loading