From d675188f4b69be91215f6e700c43d9dfae2e8602 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Wed, 7 Jan 2026 21:56:50 +0000 Subject: [PATCH 01/14] Only create one prerender chrome page for testing --- packages/realm-server/tests/helpers/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index d4b5a25bc3..ae4861ca9c 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -256,6 +256,7 @@ async function startTestPrerenderServer(): Promise { } let server = createPrerenderHttpServer({ silent: Boolean(process.env.SILENT_PRERENDERER), + maxPages: 1, }); prerenderServer = server; trackServer(server); From e1925e8183ab4f0d4e2086b79a6a4476b130cf36 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Thu, 8 Jan 2026 13:41:40 +0000 Subject: [PATCH 02/14] Construct filesystem --- packages/realm-server/tests/helpers/index.ts | 82 +++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index ae4861ca9c..fb47baa20b 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -1,13 +1,14 @@ import { writeFileSync, writeJSONSync, + readFileSync, + utimesSync, readdirSync, statSync, ensureDirSync, - copySync, } from 'fs-extra'; import { NodeAdapter } from '../../node-realm'; -import { join } from 'path'; +import { dirname, isAbsolute, join } from 'path'; import type { LooseSingleCardDocument, RealmPermissions, @@ -42,7 +43,7 @@ import { CachingDefinitionLookup, } from '@cardstack/runtime-common'; import { resetCatalogRealms } from '../../handlers/handle-fetch-catalog-realms'; -import { dirSync, setGracefulCleanup, type DirResult } from 'tmp'; +import { dirSync, file, setGracefulCleanup, type DirResult } from 'tmp'; import { getLocalConfig as getSynapseConfig } from '../../synapse'; import { RealmServer } from '../../server'; @@ -70,9 +71,11 @@ import type { import { createRemotePrerenderer } from '../../prerender/remote-prerenderer'; import { createPrerenderHttpServer } from '../../prerender/prerender-app'; import { buildCreatePrerenderAuth } from '../../prerender/auth'; +import { glob } from 'glob'; const testRealmURL = new URL('http://127.0.0.1:4444/'); const testRealmHref = testRealmURL.href; +const processStartTimeMs = Date.now(); export const testRealmServerMatrixUsername = 'node-test_realm-server'; export const testRealmServerMatrixUserId = `@${testRealmServerMatrixUsername}:localhost`; @@ -129,6 +132,61 @@ export const testRealmInfo = { lastPublishedAt: null, }; +export function buildCardFileSystem(entries: string[]): Record { + let cardsDir = join(__dirname, '..', 'cards'); + let fileSystem: Record = {}; + + for (let entry of entries) { + let normalized = entry.replace(/^\.\//, ''); + if (isAbsolute(entry) || normalized.split('/').includes('..')) { + throw new Error(`Card entry must be within ../cards: ${entry}`); + } + + let matches: string[] = []; + let fullPath = join(cardsDir, normalized); + + try { + let stats = statSync(fullPath); + if (stats.isDirectory()) { + matches = glob.sync(`${normalized}/**/*`, { + cwd: cardsDir, + nodir: true, + dot: true, + }); + } else { + matches = [normalized]; + } + } catch { + matches = glob.sync(normalized, { cwd: cardsDir, dot: true }); + } + + if (matches.length === 0) { + throw new Error(`No card files matched: ${entry}`); + } + + for (let match of matches) { + let matchPath = join(cardsDir, match); + let stats = statSync(matchPath); + if (stats.isDirectory()) { + let nestedMatches = glob.sync(`${match}/**/*`, { + cwd: cardsDir, + nodir: true, + dot: true, + }); + for (let nested of nestedMatches) { + if (!fileSystem[nested]) { + fileSystem[nested] = readFileSync(join(cardsDir, nested), 'utf8'); + } + } + } else if (!fileSystem[match]) { + fileSystem[match] = readFileSync(matchPath, 'utf8'); + } + } + } + + return fileSystem; +} + export const realmServerTestMatrix: MatrixConfig = { url: matrixURL, username: 'node-test_realm-server', @@ -421,11 +479,17 @@ export async function createRealm({ await insertPermissions(dbAdapter, new URL(realmURL), permissions); for (let [filename, contents] of Object.entries(fileSystem)) { + let fullPath = join(dir, filename); + ensureDirSync(dirname(fullPath)); + if (typeof contents === 'string') { - writeFileSync(join(dir, filename), contents); + writeFileSync(fullPath, contents); } else { - writeJSONSync(join(dir, filename), contents); + writeJSONSync(fullPath, contents); } + + let mtime = new Date(processStartTimeMs); + utimesSync(fullPath, mtime, mtime); } let adapter = new NodeAdapter(dir, enableFileWatcher); @@ -889,6 +953,9 @@ export function setupPermissionedRealm( published?: boolean; }, ) { + if (!fileSystem) { + fileSystem = buildCardFileSystem(['**/*']); + } let testRealmServer: Awaited>; setGracefulCleanup(); @@ -931,11 +998,6 @@ export function setupPermissionedRealm( ensureDirSync(testRealmDir); - // If a fileSystem is provided, use it to populate the test realm, otherwise copy the default cards - if (!fileSystem) { - copySync(join(__dirname, '..', 'cards'), testRealmDir); - } - let virtualNetwork = createVirtualNetwork(); testRealmServer = await runTestRealmServer({ From 027fa815c21fb670a8e2e3d982d4b33daf8464cb Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Sat, 10 Jan 2026 21:26:17 +0000 Subject: [PATCH 03/14] Add full checkpoint and restore for tests to speed up realm creation --- packages/realm-server/tests/helpers/index.ts | 333 ++++++++++++++++--- 1 file changed, 280 insertions(+), 53 deletions(-) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index fb47baa20b..143793dec7 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -9,6 +9,9 @@ import { } from 'fs-extra'; import { NodeAdapter } from '../../node-realm'; import { dirname, isAbsolute, join } from 'path'; +import { createHash } from 'crypto'; +import { spawn } from 'child_process'; +import { Client } from 'pg'; import type { LooseSingleCardDocument, RealmPermissions, @@ -51,6 +54,7 @@ import { PgAdapter, PgQueuePublisher, PgQueueRunner, + postgresConfig, } from '@cardstack/postgres'; import type { Server } from 'http'; import { MatrixClient } from '@cardstack/runtime-common/matrix-client'; @@ -72,11 +76,14 @@ import { createRemotePrerenderer } from '../../prerender/remote-prerenderer'; import { createPrerenderHttpServer } from '../../prerender/prerender-app'; import { buildCreatePrerenderAuth } from '../../prerender/auth'; import { glob } from 'glob'; +import { template } from 'lodash'; const testRealmURL = new URL('http://127.0.0.1:4444/'); const testRealmHref = testRealmURL.href; const processStartTimeMs = Date.now(); +const TEMPLATE_DB_PREFIX = 'test_template'; + export const testRealmServerMatrixUsername = 'node-test_realm-server'; export const testRealmServerMatrixUserId = `@${testRealmServerMatrixUsername}:localhost`; @@ -187,6 +194,169 @@ export function buildCardFileSystem(entries: string[]): Record { return fileSystem; } +function stableStringify(value: unknown): string { + let seen = new WeakSet(); + return JSON.stringify(value, (_key, val) => { + if (val && typeof val === 'object' && !Array.isArray(val)) { + if (seen.has(val)) { + return '[Circular]'; + } + seen.add(val); + let sorted: Record = {}; + for (let key of Object.keys(val).sort()) { + sorted[key] = (val as Record)[key]; + } + return sorted; + } + return val; + }); +} + +function safeDbName(name: string): string { + if (!/^[a-zA-Z0-9_]+$/.test(name)) { + throw new Error(`Unsafe database name: ${name}`); + } + return name; +} + +function adminDbConfig() { + let config = postgresConfig(); + return { ...config, database: 'postgres' }; +} + +async function runCommand( + command: string, + args: string[], + env: NodeJS.ProcessEnv, +): Promise { + await new Promise((resolve, reject) => { + let stderr = ''; + let child = spawn(command, args, { env }); + child.stderr?.on('data', (data) => { + stderr += data.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`${command} exited with code ${code}: ${stderr}`)); + } + }); + }); +} + +async function createTemplateDb(options: { + sourceDbName: string; + templateDbName: string; +}): Promise { + let client = new Client(adminDbConfig()); + let start = process.hrtime.bigint(); + let dumpPath = `/tmp/${options.templateDbName}.dump`; + await client.connect(); + try { + let existing = await client.query( + 'SELECT 1 FROM pg_database WHERE datname = $1', + [options.templateDbName], + ); + if (existing.rowCount > 0) { + return; + } + + let templateDbName = safeDbName(options.templateDbName); + let sourceDbName = safeDbName(options.sourceDbName); + await client.query(`CREATE DATABASE ${templateDbName}`); + + await runCommand( + 'docker', + [ + 'exec', + 'boxel-pg', + 'pg_dump', + '--format=custom', + '--file', + dumpPath, + '--username', + 'postgres', + sourceDbName, + ], + process.env, + ); + + await runCommand( + 'docker', + [ + 'exec', + 'boxel-pg', + 'pg_restore', + '--no-owner', + '--no-privileges', + '--username', + 'postgres', + '--dbname', + templateDbName, + dumpPath, + ], + process.env, + ); + + await client.query(`ALTER DATABASE ${templateDbName} IS_TEMPLATE true`); + await client.query( + `ALTER DATABASE ${templateDbName} WITH ALLOW_CONNECTIONS false`, + ); + } finally { + let durationMs = Number(process.hrtime.bigint() - start) / 1e6; + console.log( + `[template-db] create template ${options.templateDbName} took ${durationMs.toFixed( + 1, + )}ms`, + ); + try { + await runCommand( + 'docker', + ['exec', 'boxel-pg', 'rm', '-f', dumpPath], + process.env, + ); + } catch { + // ignore cleanup failures + } + await client.end(); + } +} + +async function restoreDbFromTemplate(options: { + targetDbName: string; + templateDbName: string; +}): Promise { + let client = new Client(adminDbConfig()); + let start = process.hrtime.bigint(); + await client.connect(); + try { + let existing = await client.query( + 'SELECT 1 FROM pg_database WHERE datname = $1', + [options.templateDbName], + ); + if (existing.rowCount === 0) { + return false; + } + + let templateDbName = safeDbName(options.templateDbName); + let targetDbName = safeDbName(options.targetDbName); + await client.query( + `CREATE DATABASE ${targetDbName} WITH TEMPLATE ${templateDbName}`, + ); + return true; + } finally { + let durationMs = Number(process.hrtime.bigint() - start) / 1e6; + console.log( + `[template-db] restore ${options.targetDbName} from ${options.templateDbName} took ${durationMs.toFixed( + 1, + )}ms`, + ); + await client.end(); + } +} + export const realmServerTestMatrix: MatrixConfig = { url: matrixURL, username: 'node-test_realm-server', @@ -367,6 +537,7 @@ export function setupDB( beforeEach?: BeforeAfterCallback; afterEach?: BeforeAfterCallback; } = {}, + templateDbName?: string, ) { let dbAdapter: PgAdapter; let publisher: QueuePublisher; @@ -374,6 +545,27 @@ export function setupDB( const runBeforeHook = async () => { prepareTestDB(); + if (templateDbName) { + let targetDbName = + process.env.PGDATABASE ?? + `test_db_${Math.floor(10000000 * Math.random())}`; + try { + let restored = await restoreDbFromTemplate({ + targetDbName, + templateDbName, + }); + if (!restored) { + console.warn( + `[template-db] template not found: ${templateDbName}, continuing without restore`, + ); + } + } catch (error) { + console.warn( + `[template-db] failed to restore from ${templateDbName}, continuing without restore`, + error, + ); + } + } dbAdapter = new PgAdapter({ autoMigrate: true }); trackedDbAdapters.add(dbAdapter); publisher = new PgQueuePublisher(dbAdapter); @@ -960,28 +1152,47 @@ export function setupPermissionedRealm( setGracefulCleanup(); - setupDB(hooks, { - [mode]: async ( - dbAdapter: PgAdapter, - publisher: QueuePublisher, - runner: QueueRunner, - ) => { - let dir = dirSync(); - - let testRealmDir; - - if (published) { - let publishedRealmId = uuidv4(); - - testRealmDir = join( - dir.name, - 'realm_server_1', - PUBLISHED_DIRECTORY_NAME, - publishedRealmId, - ); + let templateDbName = undefined; + // Only create a template DB for non-published realms + // based on their permissions and filesystem + if (!published) { + let hash = createHash('sha256') + .update( + stableStringify({ + permissions, + fileSystem, + published, + }), + ) + .digest('hex') + .slice(0, 16); + templateDbName = `${TEMPLATE_DB_PREFIX}_${hash}_${process.pid}`; + } - dbAdapter.execute( - `INSERT INTO + setupDB( + hooks, + { + [mode]: async ( + dbAdapter: PgAdapter, + publisher: QueuePublisher, + runner: QueueRunner, + ) => { + let dir = dirSync(); + + let testRealmDir; + + if (published) { + let publishedRealmId = uuidv4(); + + testRealmDir = join( + dir.name, + 'realm_server_1', + PUBLISHED_DIRECTORY_NAME, + publishedRealmId, + ); + + dbAdapter.execute( + `INSERT INTO published_realms (id, owner_username, source_realm_url, published_realm_url) VALUES @@ -991,42 +1202,58 @@ export function setupPermissionedRealm( 'http://example.localhost/source', '${testRealmHref}' )`, - ); - } else { - testRealmDir = join(dir.name, 'realm_server_1', 'test'); - } + ); + } else { + testRealmDir = join(dir.name, 'realm_server_1', 'test'); + } - ensureDirSync(testRealmDir); - - let virtualNetwork = createVirtualNetwork(); - - testRealmServer = await runTestRealmServer({ - virtualNetwork, - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_1'), - realmURL: testRealmURL, - permissions, - dbAdapter, - runner, - publisher, - matrixURL, - fileSystem, - enableFileWatcher: subscribeToRealmEvents, - }); + ensureDirSync(testRealmDir); - let request = supertest(testRealmServer.testRealmHttpServer); + let virtualNetwork = createVirtualNetwork(); - onRealmSetup?.({ - dbAdapter, - testRealm: testRealmServer.testRealm, - testRealmPath: testRealmServer.testRealmDir, - testRealmHttpServer: testRealmServer.testRealmHttpServer, - testRealmAdapter: testRealmServer.testRealmAdapter, - request, - dir, - }); + testRealmServer = await runTestRealmServer({ + virtualNetwork, + testRealmDir, + realmsRootPath: join(dir.name, 'realm_server_1'), + realmURL: testRealmURL, + permissions, + dbAdapter, + runner, + publisher, + matrixURL, + fileSystem, + enableFileWatcher: subscribeToRealmEvents, + }); + if (templateDbName) { + try { + let sourceDbName = + process.env.PGDATABASE ?? + dbAdapter.url.split('/').pop() ?? + 'boxel'; + await createTemplateDb({ + sourceDbName, + templateDbName, + }); + } catch (error) { + console.warn('[template-db] failed to create template', error); + } + } + + let request = supertest(testRealmServer.testRealmHttpServer); + + onRealmSetup?.({ + dbAdapter, + testRealm: testRealmServer.testRealm, + testRealmPath: testRealmServer.testRealmDir, + testRealmHttpServer: testRealmServer.testRealmHttpServer, + testRealmAdapter: testRealmServer.testRealmAdapter, + request, + dir, + }); + }, }, - }); + templateDbName, + ); hooks[mode === 'beforeEach' ? 'afterEach' : 'after'](async function () { testRealmServer.testRealm.unsubscribe(); From f81424b20ce22bdd0c7ad5aae6cd6d56ac0c7a54 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Mon, 12 Jan 2026 14:51:41 +0000 Subject: [PATCH 04/14] Speed up test db removal --- .../realm-server/scripts/remove-test-dbs.sh | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/realm-server/scripts/remove-test-dbs.sh b/packages/realm-server/scripts/remove-test-dbs.sh index 43028801ab..6a0ff567eb 100755 --- a/packages/realm-server/scripts/remove-test-dbs.sh +++ b/packages/realm-server/scripts/remove-test-dbs.sh @@ -10,8 +10,29 @@ for pid in $isolated_realm_processes; do kill -9 $pid done -databases=$(docker exec boxel-pg psql -U postgres -w -lqt | cut -d \| -f 1 | grep -E 'test_db_' | tr -d ' ') echo "cleaning up old test databases..." -for db in $databases; do - docker exec boxel-pg dropdb -U postgres -w $db -done +exit 0 +docker exec -i boxel-pg psql -X -U postgres -d postgres -v ON_ERROR_STOP=0 <<'SQL' +\set AUTOCOMMIT on +COMMIT; + +-- (optional) kick anyone out first (as separate statements) +SELECT format( + 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = %L AND pid <> pg_backend_pid();', + datname +) +FROM pg_database +WHERE datname ~ '^test_db_' + AND datname <> current_database() +ORDER BY datname +\gexec + +-- now drop (ONE statement per row) +SELECT format('DROP DATABASE %I;', datname) +FROM pg_database +WHERE datname ~ '^test_db_' + AND datname <> current_database() +ORDER BY datname +\gexec +SQL +echo "Cleaned up old test databases." \ No newline at end of file From cad3dd5014a3b014ef03efc5d38d518da680b6df Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Mon, 12 Jan 2026 14:53:12 +0000 Subject: [PATCH 05/14] Cleaning up test databases as we go --- packages/realm-server/tests/helpers/index.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index 143793dec7..200ec92f19 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -357,6 +357,17 @@ async function restoreDbFromTemplate(options: { } } +async function dropTestDb(dbName: string): Promise { + let client = new Client(adminDbConfig()); + await client.connect(); + try { + let safeName = safeDbName(dbName); + await client.query(`DROP DATABASE IF EXISTS ${safeName} WITH (FORCE)`); + } finally { + await client.end(); + } +} + export const realmServerTestMatrix: MatrixConfig = { url: matrixURL, username: 'node-test_realm-server', @@ -590,6 +601,17 @@ export function setupDB( if (dbAdapter) { trackedDbAdapters.delete(dbAdapter); } + + if (process.env.PGDATABASE) { + try { + await dropTestDb(process.env.PGDATABASE); + } catch (error) { + console.warn( + `[test-setup] failed to drop test db ${process.env.PGDATABASE}`, + error, + ); + } + } await stopTestPrerenderServer(); }; From 797772e1f3290b3b33363d13b549a8041b52c6dc Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 09:43:50 +0000 Subject: [PATCH 06/14] Run chunks of realm-server tests on ci --- .github/workflows/ci.yaml | 102 +++++++++--------- packages/realm-server/package.json | 3 +- .../realm-server/scripts/lint-test-shards.ts | 43 ++++++-- .../realm-server/scripts/run-test-modules.js | 57 ++++++++++ 4 files changed, 150 insertions(+), 55 deletions(-) create mode 100644 packages/realm-server/scripts/run-test-modules.js diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c8b43656be..8fed1d8ddc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -419,60 +419,66 @@ jobs: -d '{"context":"Matrix Playwright tests report","description":"'"$description"'","target_url":"'"$PLAYWRIGHT_REPORT_URL"'","state":"'"$state"'"}' realm-server-test: - name: Realm Server Tests + name: Realm Server Tests (shard ${{ matrix.shard }}) needs: [change-check, test-web-assets] if: needs.change-check.outputs.realm-server == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true' runs-on: ubuntu-latest concurrency: - group: realm-server-test-${{ matrix.testModule }}-${{ github.head_ref || github.run_id }} + group: realm-server-test-${{ matrix.shard }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true strategy: fail-fast: false matrix: - testModule: - [ - "auth-client-test.ts", - "billing-test.ts", - "card-dependencies-endpoint-test.ts", - "card-endpoints-test.ts", - "card-source-endpoints-test.ts", - "definition-lookup-test.ts", - "file-watcher-events-test.ts", - "indexing-test.ts", - "transpile-test.ts", - "module-syntax-test.ts", - "permissions/permission-checker-test.ts", - "prerendering-test.ts", - "prerender-server-test.ts", - "prerender-manager-test.ts", - "prerender-proxy-test.ts", - "remote-prerenderer-test.ts", - "queue-test.ts", - "realm-endpoints/dependencies-test.ts", - "realm-endpoints/directory-test.ts", - "realm-endpoints/info-test.ts", - "realm-endpoints/lint-test.ts", - "realm-endpoints/mtimes-test.ts", - "realm-endpoints/permissions-test.ts", - "realm-endpoints/publishability-test.ts", - "realm-endpoints/search-test.ts", - "realm-endpoints/user-test.ts", - "realm-endpoints-test.ts", - "search-prerendered-test.ts", - "types-endpoint-test.ts", - "server-endpoints-test.ts", - "server-endpoints/search-test.ts", - "virtual-network-test.ts", - "atomic-endpoints-test.ts", - "request-forward-test.ts", - "publish-unpublish-realm-test.ts", - "boxel-domain-availability-test.ts", - "claim-boxel-domain-test.ts", - "delete-boxel-claimed-domain-test.ts", - "get-boxel-claimed-domain-test.ts", - "realm-auth-test.ts", - "queries-test.ts", - ] + include: + - shard: 1 + testModules: + - server-endpoints-test.ts + - atomic-endpoints-test.ts + - request-forward-test.ts + - realm-endpoints/info-test.ts + - realm-endpoints/user-test.ts + - file-watcher-events-test.ts + - types-endpoint-test.ts + - prerender-manager-test.ts + - prerender-proxy-test.ts + - transpile-test.ts + - shard: 2 + testModules: + - card-endpoints-test.ts + - publish-unpublish-realm-test.ts + - realm-endpoints/lint-test.ts + - claim-boxel-domain-test.ts + - realm-endpoints/publishability-test.ts + - boxel-domain-availability-test.ts + - realm-endpoints/dependencies-test.ts + - queue-test.ts + - remote-prerenderer-test.ts + - permissions/permission-checker-test.ts + - shard: 3 + testModules: + - realm-endpoints-test.ts + - prerendering-test.ts + - search-prerendered-test.ts + - card-dependencies-endpoint-test.ts + - delete-boxel-claimed-domain-test.ts + - get-boxel-claimed-domain-test.ts + - realm-endpoints/mtimes-test.ts + - billing-test.ts + - virtual-network-test.ts + - module-syntax-test.ts + - shard: 4 + testModules: + - indexing-test.ts + - card-source-endpoints-test.ts + - realm-endpoints/search-test.ts + - server-endpoints/search-test.ts + - realm-endpoints/permissions-test.ts + - realm-endpoints/directory-test.ts + - definition-lookup-test.ts + - prerender-server-test.ts + - realm-auth-test.ts + - auth-client-test.ts + - queries-test.ts steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 - uses: ./.github/actions/init @@ -506,7 +512,7 @@ jobs: run: pnpm test:wait-for-servers working-directory: packages/realm-server env: - TEST_MODULE: ${{matrix.testModule}} + TEST_MODULES: ${{ join(matrix.testModules, '|') }} - name: Print realm server logs if: always() run: cat /tmp/server.log @@ -514,7 +520,7 @@ jobs: id: artifact_name if: always() run: | - export SAFE_ARTIFACT_NAME=$(echo ${{ matrix.testModule }} | sed 's/[/]/_/g') + export SAFE_ARTIFACT_NAME=shard-${{ matrix.shard }} echo "artifact_name=$SAFE_ARTIFACT_NAME" >> "$GITHUB_OUTPUT" - name: Upload realm server log uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # 4.6.1 diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index efaaab3aff..bf0b1b0ef1 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -81,11 +81,12 @@ "scripts": { "test": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only tests/index.ts", "test-module": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only --module ${TEST_MODULE} tests/index.ts", + "test-modules": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key node ./scripts/run-test-modules.js", "start:matrix": "cd ../matrix && pnpm assert-synapse-running", "start:smtp": "cd ../matrix && pnpm assert-smtp-running", "start:pg": "./scripts/start-pg.sh", "stop:pg": "./scripts/stop-pg.sh", - "test:wait-for-servers": "WAIT_ON_TIMEOUT=900000 NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson|http://localhost:8008|http://localhost:5001' 'test-module'", + "test:wait-for-servers": "WAIT_ON_TIMEOUT=900000 NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson|http://localhost:8008|http://localhost:5001' 'test-modules'", "setup:base-in-deployment": "mkdir -p /persistent/base && rsync --dry-run --itemize-changes --checksum --recursive --delete ../base/. /persistent/base/ && rsync --checksum --recursive --delete ../base/. /persistent/base/", "setup:experiments-in-deployment": "mkdir -p /persistent/experiments && rsync --dry-run --itemize-changes --checksum --recursive ../experiments-realm/. /persistent/experiments/ && rsync --checksum --recursive ../experiments-realm/. /persistent/experiments/", "setup:catalog-in-deployment": "mkdir -p /persistent/catalog && rsync --dry-run --itemize-changes --checksum --recursive --delete ../catalog-realm/. /persistent/catalog/ && rsync --checksum --recursive --delete ../catalog-realm/. /persistent/catalog/", diff --git a/packages/realm-server/scripts/lint-test-shards.ts b/packages/realm-server/scripts/lint-test-shards.ts index f0df182e83..4576ce5225 100755 --- a/packages/realm-server/scripts/lint-test-shards.ts +++ b/packages/realm-server/scripts/lint-test-shards.ts @@ -19,18 +19,49 @@ function getCiTestModules(yamlFilePath: string) { const yamlContent = readFileSync(yamlFilePath, 'utf8'); const yamlData = yaml.load(yamlContent) as Record; - const shardIndexes: string[] = - yamlData?.jobs?.['realm-server-test']?.strategy?.matrix?.testModule; + const matrix = yamlData?.jobs?.['realm-server-test']?.strategy?.matrix; + const testModules = matrix?.testModule; - if (!Array.isArray(shardIndexes)) { + if (Array.isArray(testModules)) { + return testModules; + } + + const include = matrix?.include; + if (!Array.isArray(include)) { throw new Error( - `Invalid 'jobs.realm-server-test.strategy.matrix.testModule' format in the YAML file.`, + `Invalid 'jobs.realm-server-test.strategy.matrix' format in the YAML file.`, ); } - return shardIndexes; + const modules = new Set(); + const invalidEntries: number[] = []; + include.forEach((entry: Record, index: number) => { + const entryModules = entry?.testModules; + if (Array.isArray(entryModules)) { + entryModules.forEach((moduleName: string) => modules.add(moduleName)); + return; + } + if (typeof entryModules === 'string') { + entryModules + .split(/[,\s]+/) + .filter(Boolean) + .forEach((moduleName) => modules.add(moduleName)); + return; + } + invalidEntries.push(index); + }); + + if (invalidEntries.length > 0) { + throw new Error( + `Invalid 'jobs.realm-server-test.strategy.matrix.include[*].testModules' entries at indexes: ${invalidEntries.join(', ')}`, + ); + } + + return Array.from(modules); } catch (error: any) { - console.error(`Error reading shardIndex from YAML file: ${error.message}`); + console.error( + `Error reading test modules from YAML file: ${error.message}`, + ); process.exit(1); } } diff --git a/packages/realm-server/scripts/run-test-modules.js b/packages/realm-server/scripts/run-test-modules.js new file mode 100644 index 0000000000..cf02471381 --- /dev/null +++ b/packages/realm-server/scripts/run-test-modules.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +'use strict'; + +const { spawnSync } = require('node:child_process'); + +function buildModuleFilter(modulesToMatch) { + const escaped = modulesToMatch + .map((moduleName) => escapeRegex(moduleName)) + .join('|'); + const pattern = `^(?:${escaped})(?:\\s>\\s|:)`; + return `/${pattern}/`; +} + +function escapeRegex(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\//g, '\\/'); +} + +const rawModules = process.env.TEST_MODULES ?? ''; +const cleanedRaw = rawModules.trim(); + +if (!cleanedRaw) { + console.error('TEST_MODULES must be set.'); + process.exit(1); +} + +const modules = cleanedRaw + .split(/[|,]/) + .map((value) => value.trim()) + .filter(Boolean) + .map((value) => value.replace(/^['"]+|['"]+$/g, '')); + +if (modules.length === 0) { + console.error('No module names found in TEST_MODULES.'); + process.exit(1); +} + +const args = ['--require', 'ts-node/register/transpile-only']; + +args.push('--filter', buildModuleFilter(modules)); + +args.push('tests/index.ts'); + +const qunitBin = require.resolve('qunit/bin/qunit.js'); +const result = spawnSync(process.execPath, [qunitBin, ...args], { + stdio: 'inherit', + env: process.env, +}); + +if (typeof result.status === 'number') { + process.exit(result.status); +} + +if (result.error) { + console.error(result.error); +} + +process.exit(1); From f1139a3ca05544f0e27847cedcdf394b7e1df750 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 12:02:45 +0000 Subject: [PATCH 07/14] Run multiple modules as a group in CI --- README.md | 13 ++++++----- packages/realm-server/package.json | 1 - packages/realm-server/tests/index.ts | 33 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9a8d18a99c..f94bfbda4c 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ You can also use `start:development` if you want the functionality of `start:all Optional environment variables for `start:development`: -- `USE_EXTERNAL_CATALOG=1` to load `/catalog` from `packages/catalog/contents` (cloned from the `boxel-catalog` repo). +- `USE_EXTERNAL_CATALOG=1` to load `/catalog` from `packages/catalog/contents` (cloned from the `boxel-catalog` repo). ### Card Pre-rendering @@ -394,12 +394,15 @@ The tests are available at `http://localhost:4200/tests` ### Realm Server Node tests -First make sure to generate the host app's `dist/` output in order to support card pre-rendering by first starting the host app (instructions above). If you want to make the host app's `dist/` output without starting the host app, you can run `pnpm build` in the host app's workspace. - To run the `packages/realm-server/` workspace tests start: -1. `pnpm start:all` in the `packages/realm-server/` to serve _both_ the base realm and the realm that serves the test cards for node. -2. Run `pnpm test` in the `packages/realm-server/` workspace to run the realm node tests. `TEST_MODULE=realm-endpoints-test.ts pnpm test-module` is an example of how to run a single test module. +1. The host application on port 4200. You can do this by running `pnpm start` in the `packages/host/` workspace, or if you have a built folder you can serve it with a static server with `pnpm serve:dist`. +2. The base realm and associated workers, postgres and synapse. You can do this by running `pnpm start:all` in the `packages/realm-server/` workspace, or `pnpm:start-services-for-host-tests` for a more lightweight setup. +3. Run the realm server tests: + +- `pnpm test` in the `packages/realm-server/` workspace to run the realm node tests in full (~1hr). +- `TEST_MODULES="types-endpoint-test.ts|another-test-module.ts" pnpm test` in the `packages/realm-server/` workspace to run tests for a subset of modules. +- `TEST_MODULE="types-endpoint-test.ts" pnpm test-module` in the `packages/realm-server/` workspace to run tests for a specific module. ### Boxel UI diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index bf0b1b0ef1..bbbf3facba 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -81,7 +81,6 @@ "scripts": { "test": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only tests/index.ts", "test-module": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key qunit --require ts-node/register/transpile-only --module ${TEST_MODULE} tests/index.ts", - "test-modules": "./scripts/remove-test-dbs.sh; LOG_LEVELS=\"*=error,prerenderer-chrome=silent,pg-adapter=warn,realm:requests=warn${LOG_LEVELS:+,}${LOG_LEVELS}\" NODE_NO_WARNINGS=1 PGPORT=5435 STRIPE_WEBHOOK_SECRET=stripe-webhook-secret STRIPE_API_KEY=stripe-api-key node ./scripts/run-test-modules.js", "start:matrix": "cd ../matrix && pnpm assert-synapse-running", "start:smtp": "cd ../matrix && pnpm assert-smtp-running", "start:pg": "./scripts/start-pg.sh", diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index a96d6da1d5..21dc42b720 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -30,6 +30,21 @@ import * as ContentTagGlobal from 'content-tag'; import QUnit from 'qunit'; QUnit.config.testTimeout = 60000; +const testModules = process.env.TEST_MODULES?.trim(); + +if (testModules) { + const modules = parseModules(testModules); + if (modules.length > 0) { + QUnit.config.filter = buildModuleFilter(modules); + console.log( + `Filtering tests to modules from TEST_MODULES: ${modules.join(', ')}`, + ); + } else { + console.warn( + 'TEST_MODULES was provided but no module names were parsed. Running full suite.', + ); + } +} // Cleanup here ensures lingering servers/prerenderers/queues don't keep the // Node event loop alive after tests finish. @@ -155,3 +170,21 @@ import './delete-boxel-claimed-domain-test'; import './realm-auth-test'; import './queries-test'; import './remote-prerenderer-test'; + +function parseModules(value: string): string[] { + return value + .split(/[|,]/) + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => entry.replace(/^['"]+|['"]+$/g, '')); +} + +function buildModuleFilter(modulesToMatch: string[]): string { + const escaped = modulesToMatch.map((moduleName) => escapeRegex(moduleName)); + const pattern = `^(?:${escaped.join('|')})(?:\\s>\\s|:)`; + return `/${pattern}/`; +} + +function escapeRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\//g, '\\/'); +} From 3cc9065ab09be89f63e4391ed9acca8367840832 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 12:21:36 +0000 Subject: [PATCH 08/14] Call original test command --- packages/realm-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index bbbf3facba..479a5a4c3e 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -85,7 +85,7 @@ "start:smtp": "cd ../matrix && pnpm assert-smtp-running", "start:pg": "./scripts/start-pg.sh", "stop:pg": "./scripts/stop-pg.sh", - "test:wait-for-servers": "WAIT_ON_TIMEOUT=900000 NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson|http://localhost:8008|http://localhost:5001' 'test-modules'", + "test:wait-for-servers": "WAIT_ON_TIMEOUT=900000 NODE_NO_WARNINGS=1 start-server-and-test 'pnpm run wait' 'http-get://localhost:4201/base/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson' 'pnpm run wait' 'http-get://localhost:4202/node-test/_readiness-check?acceptHeader=application%2Fvnd.api%2Bjson|http://localhost:8008|http://localhost:5001' 'test'", "setup:base-in-deployment": "mkdir -p /persistent/base && rsync --dry-run --itemize-changes --checksum --recursive --delete ../base/. /persistent/base/ && rsync --checksum --recursive --delete ../base/. /persistent/base/", "setup:experiments-in-deployment": "mkdir -p /persistent/experiments && rsync --dry-run --itemize-changes --checksum --recursive ../experiments-realm/. /persistent/experiments/ && rsync --checksum --recursive ../experiments-realm/. /persistent/experiments/", "setup:catalog-in-deployment": "mkdir -p /persistent/catalog && rsync --dry-run --itemize-changes --checksum --recursive --delete ../catalog-realm/. /persistent/catalog/ && rsync --checksum --recursive --delete ../catalog-realm/. /persistent/catalog/", From b0c50db13f0dadf8819a99de96a1328975cfd172 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 13:58:12 +0000 Subject: [PATCH 09/14] Remove unused extra realm server --- .../tests/realm-endpoints-test.ts | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/packages/realm-server/tests/realm-endpoints-test.ts b/packages/realm-server/tests/realm-endpoints-test.ts index 711945ff7c..21a139d652 100644 --- a/packages/realm-server/tests/realm-endpoints-test.ts +++ b/packages/realm-server/tests/realm-endpoints-test.ts @@ -60,8 +60,6 @@ import type { RealmEventContent, } from 'https://cardstack.com/base/matrix-event'; -const testRealm2URL = new URL('http://127.0.0.1:4445/test/'); - module(basename(__filename), function () { module('Realm-specific Endpoints', function (hooks) { let testRealm: Realm; @@ -69,12 +67,6 @@ module(basename(__filename), function () { let request: SuperTest; let dir: DirResult; let dbAdapter: PgAdapter; - let testRealmHttpServer2: Server; - let testRealm2: Realm; - let dbAdapter2: PgAdapter; - let publisher: QueuePublisher; - let runner: QueueRunner; - let testRealmDir: string; function onRealmSetup(args: { testRealm: Realm; @@ -110,45 +102,6 @@ module(basename(__filename), function () { }); let { getMessagesSince } = setupMatrixRoom(hooks, getRealmSetup); - let virtualNetwork = createVirtualNetwork(); - - async function startRealmServer( - dbAdapter: PgAdapter, - publisher: QueuePublisher, - runner: QueueRunner, - ) { - if (testRealm2) { - virtualNetwork.unmount(testRealm2.handle); - } - ({ testRealm: testRealm2, testRealmHttpServer: testRealmHttpServer2 } = - await runTestRealmServer({ - virtualNetwork, - testRealmDir, - realmsRootPath: join(dir.name, 'realm_server_2'), - realmURL: testRealm2URL, - dbAdapter, - publisher, - runner, - matrixURL, - })); - - await testRealm.logInToMatrix(); - } - - setupDB(hooks, { - beforeEach: async (_dbAdapter, _publisher, _runner) => { - dbAdapter2 = _dbAdapter; - publisher = _publisher; - runner = _runner; - testRealmDir = join(dir.name, 'realm_server_2', 'test'); - ensureDirSync(testRealmDir); - copySync(join(__dirname, 'cards'), testRealmDir); - await startRealmServer(dbAdapter2, publisher, runner); - }, - afterEach: async () => { - await closeServer(testRealmHttpServer2); - }, - }); test('can set response ETag and Cache-Control headers for module request', async function (assert) { let response = await request From ca679537f4380e29e3302745e74991624b38daa8 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 14:20:41 +0000 Subject: [PATCH 10/14] Improve indexing RO test performance --- packages/realm-server/tests/indexing-test.ts | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index ceb4d6aa55..c231281dbc 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -22,6 +22,7 @@ import { closeServer, setupPermissionedRealms, cardInfo, + setupPermissionedRealm, } from './helpers'; import stripScopedCSSAttributes from '@cardstack/runtime-common/helpers/strip-scoped-css-attributes'; import { join, basename } from 'path'; @@ -441,7 +442,7 @@ async function stopTestRealm(testRealmServer?: TestRealmServerResult) { module(basename(__filename), function () { module('indexing (read only)', function (hooks) { let realm: Realm; - let testRealmServer: TestRealmServerResult | undefined; + let testDbAdapter: PgAdapter; async function getInstance( realm: Realm, @@ -454,20 +455,18 @@ module(basename(__filename), function () { return maybeInstance as IndexedInstance | undefined; } - setupDB(hooks, { - before: async (dbAdapter, publisher, runner) => { - testDbAdapter = dbAdapter; - testRealmServer = await startTestRealm({ - dbAdapter, - publisher, - runner, - }); - realm = testRealmServer.testRealm; - }, - after: async () => { - await stopTestRealm(testRealmServer); - testRealmServer = undefined; + function onRealmSetup(args: { testRealm: Realm; dbAdapter: PgAdapter }) { + realm = args.testRealm; + testDbAdapter = args.dbAdapter; + } + + setupPermissionedRealm(hooks, { + permissions: { + '*': ['read'], }, + realmURL: testRealm, + fileSystem: makeTestRealmFileSystem(), + onRealmSetup, }); test('realm is full indexed at boot', async function (assert) { From 18f46fa10a09be5b46eb8b245a465d1c8eb6cc61 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 20:36:54 +0000 Subject: [PATCH 11/14] Speed up indexing tests --- packages/realm-server/tests/indexing-test.ts | 117 +++++++------------ 1 file changed, 41 insertions(+), 76 deletions(-) diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index c231281dbc..577da0f501 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -42,11 +42,8 @@ function trimCardContainer(text: string) { ); } -let testDbAdapter: DBAdapter; const testRealm = new URL('http://127.0.0.1:4445/test/'); -type TestRealmServerResult = Awaited>; - function makeTestRealmFileSystem(): Record< string, string | LooseSingleCardDocument @@ -404,41 +401,6 @@ function makeTestRealmFileSystem(): Record< }; } -async function startTestRealm({ - dbAdapter, - publisher, - runner, -}: { - dbAdapter: DBAdapter; - publisher: QueuePublisher; - runner: QueueRunner; -}): Promise { - let virtualNetwork = createVirtualNetwork(); - let dir = dirSync().name; - let testRealmServer = await runTestRealmServer({ - testRealmDir: dir, - realmsRootPath: join(dir, 'realm_server_1'), - virtualNetwork, - realmURL: testRealm, - dbAdapter: dbAdapter as PgAdapter, - publisher: publisher as PgQueuePublisher, - runner: runner as PgQueueRunner, - matrixURL, - fileSystem: makeTestRealmFileSystem(), - }); - await testRealmServer.testRealm.start(); - return testRealmServer; -} - -async function stopTestRealm(testRealmServer?: TestRealmServerResult) { - if (!testRealmServer) { - return; - } - testRealmServer.testRealm.unsubscribe(); - await closeServer(testRealmServer.testRealmHttpServer); - resetCatalogRealms(); -} - module(basename(__filename), function () { module('indexing (read only)', function (hooks) { let realm: Realm; @@ -892,23 +854,24 @@ module(basename(__filename), function () { module('indexing (mutating)', function (hooks) { let realm: Realm; let adapter: RealmAdapter; - let testRealmServer: TestRealmServerResult | undefined; - - setupDB(hooks, { - beforeEach: async (dbAdapter, publisher, runner) => { - testDbAdapter = dbAdapter; - testRealmServer = await startTestRealm({ - dbAdapter, - publisher, - runner, - }); - realm = testRealmServer.testRealm; - adapter = testRealmServer.testRealmAdapter; - }, - afterEach: async () => { - await stopTestRealm(testRealmServer); - testRealmServer = undefined; - }, + let testDbAdapter: PgAdapter; + + function onRealmSetup(args: { + testRealm: Realm; + testRealmAdapter: RealmAdapter; + dbAdapter: PgAdapter; + }) { + realm = args.testRealm; + adapter = args.testRealmAdapter; + testDbAdapter = args.dbAdapter; + } + setupPermissionedRealm(hooks, { + permissions: { + '*': ['read', 'write'], + } as RealmPermissions, + realmURL: testRealm, + fileSystem: makeTestRealmFileSystem(), + onRealmSetup, }); test('can incrementally index updated instance', async function (assert) { @@ -1230,29 +1193,31 @@ module(basename(__filename), function () { 'the deleted type results in no card instance results', ); } - let actual = await realm.realmIndexQueryEngine.cardDocument( - new URL(`${testRealm}post-1`), - ); - if (actual?.type === 'error') { - assert.ok(actual.error.errorDetail.stack, 'stack trace is included'); - delete actual.error.errorDetail.stack; - assert.deepEqual( - // we splat because despite having the same shape, the constructors are different - { ...actual.error.errorDetail }, - { - id: `${testRealm}post`, - isCardError: true, - additionalErrors: null, - message: `missing file ${testRealm}post`, - status: 404, - title: 'Link Not Found', - deps: [`${testRealm}post`], - }, - 'card instance is an error document', + // Wait until the error document has a stack trace as expected + let retries = 10; + let hasStackTrace = false; + while (retries > 0 && !hasStackTrace) { + let actual = await realm.realmIndexQueryEngine.cardDocument( + new URL(`${testRealm}post-1`), ); - } else { - assert.ok(false, 'search index entry is not an error document'); + if (actual?.type === 'error') { + // Should have a stack trace + if ( + actual.error.errorDetail.stack && + actual.error.errorDetail.message === + `missing file ${testRealm}post` && + actual.error.errorDetail.id === `${testRealm}post` + ) { + hasStackTrace = true; + break; + } + } + // Wait and check again + console.log('waiting for error document to have stack trace...'); + retries--; + await new Promise((resolve) => setTimeout(resolve, 500)); } + assert.ok(hasStackTrace, 'error document has stack trace as expected'); // when the definitions is created again, the instance should mend its broken link await realm.write( From c80f78decb2b9bbf44b2168e41c7609bbc2a4202 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 20:39:49 +0000 Subject: [PATCH 12/14] Remove unused code --- packages/realm-server/tests/indexing-test.ts | 22 +++----------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index 577da0f501..93069a13c9 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -1,37 +1,21 @@ import { module, test } from 'qunit'; -import { dirSync } from 'tmp'; import { SupportedMimeType } from '@cardstack/runtime-common'; import type { - DBAdapter, LooseSingleCardDocument, Realm, RealmPermissions, RealmAdapter, } from '@cardstack/runtime-common'; -import type { - IndexedInstance, - QueuePublisher, - QueueRunner, -} from '@cardstack/runtime-common'; +import type { IndexedInstance } from '@cardstack/runtime-common'; import { - setupDB, - createVirtualNetwork, - matrixURL, cleanWhiteSpace, - runTestRealmServer, - closeServer, setupPermissionedRealms, cardInfo, setupPermissionedRealm, } from './helpers'; import stripScopedCSSAttributes from '@cardstack/runtime-common/helpers/strip-scoped-css-attributes'; -import { join, basename } from 'path'; -import { resetCatalogRealms } from '../handlers/handle-fetch-catalog-realms'; -import type { - PgQueueRunner, - PgAdapter, - PgQueuePublisher, -} from '@cardstack/postgres'; +import { basename } from 'path'; +import type { PgAdapter } from '@cardstack/postgres'; function trimCardContainer(text: string) { return cleanWhiteSpace(text) From 3b040affdba2637cd54fd4ccfd6de6810f59f23d Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 13 Jan 2026 20:43:59 +0000 Subject: [PATCH 13/14] Remove some extraneous logging --- packages/realm-server/tests/helpers/index.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/realm-server/tests/helpers/index.ts b/packages/realm-server/tests/helpers/index.ts index f6ca6fca7f..2ec3d4752f 100644 --- a/packages/realm-server/tests/helpers/index.ts +++ b/packages/realm-server/tests/helpers/index.ts @@ -251,7 +251,6 @@ async function createTemplateDb(options: { templateDbName: string; }): Promise { let client = new Client(adminDbConfig()); - let start = process.hrtime.bigint(); let dumpPath = `/tmp/${options.templateDbName}.dump`; await client.connect(); try { @@ -305,12 +304,6 @@ async function createTemplateDb(options: { `ALTER DATABASE ${templateDbName} WITH ALLOW_CONNECTIONS false`, ); } finally { - let durationMs = Number(process.hrtime.bigint() - start) / 1e6; - console.log( - `[template-db] create template ${options.templateDbName} took ${durationMs.toFixed( - 1, - )}ms`, - ); try { await runCommand( 'docker', @@ -329,7 +322,6 @@ async function restoreDbFromTemplate(options: { templateDbName: string; }): Promise { let client = new Client(adminDbConfig()); - let start = process.hrtime.bigint(); await client.connect(); try { let existing = await client.query( @@ -347,12 +339,6 @@ async function restoreDbFromTemplate(options: { ); return true; } finally { - let durationMs = Number(process.hrtime.bigint() - start) / 1e6; - console.log( - `[template-db] restore ${options.targetDbName} from ${options.templateDbName} took ${durationMs.toFixed( - 1, - )}ms`, - ); await client.end(); } } From f6e5bd0accde448d18d1296277d159db8f7084b0 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Wed, 14 Jan 2026 09:28:00 +0000 Subject: [PATCH 14/14] Remove db removal skip --- packages/realm-server/scripts/remove-test-dbs.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/realm-server/scripts/remove-test-dbs.sh b/packages/realm-server/scripts/remove-test-dbs.sh index 6a0ff567eb..1b7c07fd13 100755 --- a/packages/realm-server/scripts/remove-test-dbs.sh +++ b/packages/realm-server/scripts/remove-test-dbs.sh @@ -11,7 +11,6 @@ for pid in $isolated_realm_processes; do done echo "cleaning up old test databases..." -exit 0 docker exec -i boxel-pg psql -X -U postgres -d postgres -v ON_ERROR_STOP=0 <<'SQL' \set AUTOCOMMIT on COMMIT;