Skip to content
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
18 changes: 12 additions & 6 deletions mise-tasks/services/realm-server-base
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
#!/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

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" \
MATRIX_URL="${MATRIX_URL_VAL}" \
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/"
8 changes: 4 additions & 4 deletions mise-tasks/services/worker-base
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
#!/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 \
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/"
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}"
4 changes: 2 additions & 2 deletions mise-tasks/test-services-matrix
Original file line number Diff line number Diff line change
Expand Up @@ -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'
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 });
}
6 changes: 4 additions & 2 deletions packages/matrix/docker/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isEnvironmentMode,
getSynapseContainerName,
getSynapseURL,
registerSynapseWithTraefik,
registerServiceWithTraefik,
} from '../../helpers/environment-config';

export const SYNAPSE_IP_ADDRESS = '172.20.0.5';
Expand Down Expand Up @@ -110,6 +110,7 @@ interface StartOptions {
dataDir?: string;
containerName?: string;
suppressRegistrationSecretFile?: true;
traefikServiceName?: string;
}
export async function synapseStart(
opts?: StartOptions,
Expand Down Expand Up @@ -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 };
Expand Down
35 changes: 28 additions & 7 deletions packages/matrix/helpers/environment-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand All @@ -83,9 +92,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}`;
Expand All @@ -111,28 +122,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');
Expand Down
29 changes: 22 additions & 7 deletions packages/matrix/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ 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 {
isEnvironmentMode,
getEnvironmentSlug,
} from './environment-config';
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 = isEnvironmentMode()
? `http://smtp-test.${getEnvironmentSlug()}.localhost`
: 'http://localhost:5001';
export const initialRoomName = 'New AI Assistant Chat';
export const REGISTRATION_TOKEN = 'abc123';

Expand Down Expand Up @@ -108,15 +120,18 @@ async function registerRealmRedirect(
}

export async function setRealmRedirects(page: Page) {
let baseServerUrl = isEnvironmentMode()
? `http://realm-server.${getEnvironmentSlug()}.localhost`
: '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/`,
);
}

Expand Down
Loading
Loading