Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
16cc46a
Experiment dump of e2e software construction and testing
IanCal Mar 12, 2026
107444b
Merge remote-tracking branch 'origin/main' into ian-highly-experiment…
habdelra Mar 13, 2026
078a1f2
Add software factory realm test harness
IanCal Mar 13, 2026
077cb56
Harden software factory harness startup
IanCal Mar 13, 2026
9f857db
Reuse browser cache across software factory tests
IanCal Mar 13, 2026
f8ede0f
Share prerender server across software factory tests
IanCal Mar 13, 2026
28b5da5
Simplify software factory realm harness
IanCal Mar 13, 2026
8016504
Allow cached realm boots to skip full reindex
IanCal Mar 13, 2026
4dbb3dc
Reuse support services across factory test runs
IanCal Mar 13, 2026
44761ed
Merge remote-tracking branch 'origin/main' into ian-highly-experiment…
habdelra Mar 13, 2026
8a28cf5
Merge branch 'ian-highly-experimental-dark-factory' of https://github…
habdelra Mar 13, 2026
be2ee8c
add orchestration plan and move factory into the shared realm
habdelra Mar 13, 2026
3d9f514
Merge remote-tracking branch 'origin/main' into ian-highly-experiment…
habdelra Mar 13, 2026
96dbd02
add testing strategy
habdelra Mar 13, 2026
4734ea3
normalizing software factory package and adding tests
habdelra Mar 16, 2026
c19d18f
fixed tests
habdelra Mar 16, 2026
f9d4d0b
fix types and lint
habdelra Mar 16, 2026
341c626
fix review comments
habdelra Mar 16, 2026
4945a5c
review feedback and debug logging for tests
habdelra Mar 16, 2026
5801012
more debug logging for tests
habdelra Mar 16, 2026
461a534
remove debug logging
habdelra Mar 16, 2026
2a0959b
more debug logging and enabling glint in software factory
habdelra Mar 16, 2026
f179c88
fix lint
habdelra Mar 16, 2026
9f05f64
updated lock file
habdelra Mar 16, 2026
9e865e3
Merge remote-tracking branch 'origin/main' into ian-highly-experiment…
habdelra Mar 16, 2026
1e9e74f
fix tests
habdelra Mar 16, 2026
66c594c
fix lint
habdelra Mar 16, 2026
0e0af0c
cleanup realm URL
habdelra Mar 16, 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
62 changes: 61 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
boxel-ui: ${{ steps.filter.outputs.boxel-ui }}
matrix: ${{ steps.filter.outputs.matrix }}
realm-server: ${{ steps.filter.outputs.realm-server }}
software-factory: ${{ steps.filter.outputs.software-factory }}
vscode-boxel-tools: ${{ steps.filter.outputs.vscode-boxel-tools }}
workspace-sync-cli: ${{ steps.filter.outputs.workspace-sync-cli }}
# Force all tests to run when on ci-bisect* branches
Expand Down Expand Up @@ -96,6 +97,15 @@ jobs:
- 'packages/eslint-plugin-boxel/**'
- 'packages/postgres/**'
- 'packages/realm-server/**'
software-factory:
- *shared
- 'packages/base/**'
- 'packages/boxel-icons/**'
- 'packages/host/**'
- 'packages/matrix/**'
- 'packages/postgres/**'
- 'packages/realm-server/**'
- 'packages/software-factory/**'
vscode-boxel-tools:
- *shared
- 'packages/vscode-boxel-tools/**'
Expand All @@ -111,7 +121,7 @@ jobs:
test-web-assets:
name: Build test web assets
needs: change-check
if: needs.change-check.outputs.boxel == 'true' || needs.change-check.outputs.boxel-ui == 'true' || needs.change-check.outputs.matrix == 'true' || needs.change-check.outputs.realm-server == 'true' || needs.change-check.outputs.vscode-boxel-tools == 'true' || needs.change-check.outputs.workspace-sync-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true'
if: needs.change-check.outputs.boxel == 'true' || needs.change-check.outputs.boxel-ui == 'true' || needs.change-check.outputs.matrix == 'true' || needs.change-check.outputs.realm-server == 'true' || needs.change-check.outputs.software-factory == 'true' || needs.change-check.outputs.vscode-boxel-tools == 'true' || needs.change-check.outputs.workspace-sync-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true'
uses: ./.github/workflows/test-web-assets.yaml

ai-bot-test:
Expand Down Expand Up @@ -633,6 +643,56 @@ jobs:
path: /tmp/test-realms.log
retention-days: 30

software-factory-test:
name: Software Factory Tests
needs: [change-check, test-web-assets]
if: needs.change-check.outputs.software-factory == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true'
runs-on: ubuntu-latest
concurrency:
group: software-factory-test-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- uses: ./.github/actions/init
- name: Download test web assets
uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # 4.2.0
with:
name: ${{ needs.test-web-assets.outputs.artifact_name }}
path: .test-web-assets-artifact
- name: Restore test web assets into workspace
shell: bash
run: |
shopt -s dotglob
cp -a .test-web-assets-artifact/. ./
- name: Install Playwright Browsers
run: pnpm exec playwright install
working-directory: packages/software-factory
- name: Serve host dist (test assets)
uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 # 1.0.7
with:
run: pnpm serve:dist &
working-directory: packages/host
wait-for: 3m
wait-on: http-get://localhost:4200
- name: Serve boxel-icons
uses: JarvusInnovations/background-action@2428e7b970a846423095c79d43f759abf979a635 # 1.0.7
with:
run: pnpm serve &
working-directory: packages/boxel-icons
wait-for: 3m
wait-on: http-get://localhost:4206/@cardstack/boxel-icons/v1/icons/code.js
- name: Run Playwright tests
run: pnpm test:playwright
working-directory: packages/software-factory
- name: Upload Playwright traces
if: ${{ !cancelled() }}
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # 4.6.1
with:
name: software-factory-playwright-traces
path: packages/software-factory/test-results/**/trace.zip
retention-days: 30
if-no-files-found: ignore

vscode-boxel-tools-package:
name: Boxel Tools VS Code Extension package
needs: [change-check, test-web-assets]
Expand Down
10 changes: 3 additions & 7 deletions packages/matrix/docker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import * as os from 'os';
import * as childProcess from 'child_process';
import * as fse from 'fs-extra';

function dockerPull(
image: string,
retries = 3,
delayMs = 5000,
): Promise<void> {
function dockerPull(image: string, retries = 3, delayMs = 5000): Promise<void> {
return new Promise<void>((resolve, reject) => {
let attempt = 0;
function tryPull() {
Expand Down Expand Up @@ -151,8 +147,8 @@ export async function dockerLogs(args: {
.once('close', resolve);
});

if (args.stdoutFile) await fse.close(<number>stdoutFile);
if (args.stderrFile) await fse.close(<number>stderrFile);
if (args.stdoutFile) await fse.close(stdoutFile as number);
if (args.stderrFile) await fse.close(stderrFile as number);
}

export function dockerStop(args: { containerId: string }): Promise<void> {
Expand Down
25 changes: 16 additions & 9 deletions packages/matrix/docker/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ export async function synapseStart(
opts?.template ?? 'test',
opts?.dataDir,
);
let containerName = opts?.containerName || (isEnvironmentMode() ? getSynapseContainerName() : path.basename(synCfg.configDir));
let containerName =
opts?.containerName ||
(isEnvironmentMode()
? getSynapseContainerName()
: path.basename(synCfg.configDir));
console.log(
`Starting synapse with config dir ${synCfg.configDir} in container ${containerName}...`,
);
Expand Down Expand Up @@ -381,7 +385,13 @@ export interface UpdateUserOptions {
export async function updateUser(
adminAccessToken: string,
userId: string,
{ password, displayname, avatar_url, emailAddresses, matrixURL }: UpdateUserOptions,
{
password,
displayname,
avatar_url,
emailAddresses,
matrixURL,
}: UpdateUserOptions,
) {
let url = matrixURL
? `${matrixURL}/_synapse/admin/v2/users/${userId}`
Expand Down Expand Up @@ -506,14 +516,11 @@ export async function getRoomMembers(roomId: string, accessToken: string) {
}

export async function sync(accessToken: string) {
let response = await fetch(
`${getSynapseURL()}/_matrix/client/v3/sync`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
let response = await fetch(`${getSynapseURL()}/_matrix/client/v3/sync`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
);
});
return await response.json();
}

Expand Down
7 changes: 6 additions & 1 deletion packages/matrix/helpers/isolated-realm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,23 @@ export async function startPrerenderServer(
): Promise<RunningPrerenderServer> {
let port = await findAvailablePort(options?.port ?? DEFAULT_PRERENDER_PORT);
let url = `http://localhost:${port}`;
let silent = process.env.SOFTWARE_FACTORY_PRERENDER_SILENT !== '0';
let env = {
...process.env,
NODE_ENV: process.env.NODE_ENV ?? 'development',
NODE_NO_WARNINGS: '1',
BOXEL_HOST_URL: process.env.HOST_URL ?? 'http://localhost:4200',
LOG_LEVELS:
process.env.SOFTWARE_FACTORY_PRERENDER_LOG_LEVELS ?? process.env.LOG_LEVELS,
};
let prerenderArgs = [
'--transpileOnly',
'prerender/prerender-server',
`--port=${port}`,
'--silent',
];
if (silent) {
prerenderArgs.push('--silent');
}

let child = spawn('ts-node', prerenderArgs, {
cwd: realmServerDir,
Expand Down
45 changes: 27 additions & 18 deletions packages/realm-server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ if (process.env.DISABLE_MODULE_CACHING === 'true') {
}

const ENABLE_FILE_WATCHER = process.env.ENABLE_FILE_WATCHER === 'true';
const FULL_INDEX_ON_STARTUP =
process.env.REALM_SERVER_FULL_INDEX_ON_STARTUP !== 'false';

let {
port,
Expand Down Expand Up @@ -240,7 +242,7 @@ let autoMigrate = migrateDB || undefined;
log.info(
`Realm server boot config: port=${port} serverURL=${serverURL} distURL=${distURL} matrixURL=${matrixURL} realmsRootPath=${realmsRootPath} migrateDB=${Boolean(
migrateDB,
)} workerManagerPort=${workerManagerPort ?? 'none'} prerendererUrl=${prerendererUrl} enableFileWatcher=${ENABLE_FILE_WATCHER}`,
)} workerManagerPort=${workerManagerPort ?? 'none'} prerendererUrl=${prerendererUrl} enableFileWatcher=${ENABLE_FILE_WATCHER} fullIndexOnStartup=${FULL_INDEX_ON_STARTUP}`,
);
log.info(`Realm paths: ${paths.map(String).join(', ')}`);

Expand Down Expand Up @@ -323,7 +325,7 @@ const getIndexHTML = async () => {
),
},
{
fullIndexOnStartup: true,
...(FULL_INDEX_ON_STARTUP ? { fullIndexOnStartup: true as const } : {}),
...(process.env.DISABLE_MODULE_CACHING === 'true'
? { disableModuleCaching: true }
: {}),
Expand Down Expand Up @@ -388,24 +390,27 @@ const getIndexHTML = async () => {
registerService(httpServer, serviceName, { wildcardSubdomains: true });
}
});
let stopRealmServer = (notifyParent = false) => {
let stopPort =
(httpServer.address() as import('net').AddressInfo | null)?.port ?? port;
console.log(`stopping realm server on port ${stopPort}...`);
if (isEnvironmentMode()) {
deregisterEnvironment();
}
httpServer.closeAllConnections();
httpServer.close(() => {
queue.destroy(); // warning this is async
dbAdapter.close(); // warning this is async
console.log(`realm server on port ${stopPort} has stopped`);
if (notifyParent && process.send) {
process.send('stopped');
}
process.exit(0);
});
};
process.on('message', (message) => {
if (message === 'stop') {
let stopPort =
(httpServer.address() as import('net').AddressInfo | null)?.port ??
port;
console.log(`stopping realm server on port ${stopPort}...`);
if (isEnvironmentMode()) {
deregisterEnvironment();
}
httpServer.closeAllConnections();
httpServer.close(() => {
queue.destroy(); // warning this is async
dbAdapter.close(); // warning this is async
console.log(`realm server on port ${stopPort} has stopped`);
if (process.send) {
process.send('stopped');
}
});
stopRealmServer(true);
} else if (message === 'kill') {
console.log(`Ending server process...`);
process.exit(0);
Expand Down Expand Up @@ -437,6 +442,10 @@ const getIndexHTML = async () => {
});
}
});
process.on('disconnect', () => {
console.log(`realm server IPC disconnected, shutting down...`);
stopRealmServer(false);
});

await server.start();

Expand Down
18 changes: 16 additions & 2 deletions packages/realm-server/tests/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,22 @@ export const realmServerTestMatrix: MatrixConfig = {
export const realmServerSecretSeed = "mum's the word";
export const realmSecretSeed = `shhh! it's a secret`;
export const grafanaSecret = `shhh! it's a secret`;
export const matrixRegistrationSecret: string =
getSynapseConfig()!.registration_shared_secret; // as long as synapse has been started at least once, this will always exist

function getMatrixRegistrationSecret(): string {
let secret =
getSynapseConfig()?.registration_shared_secret ??
process.env.MATRIX_REGISTRATION_SHARED_SECRET;

if (!secret) {
throw new Error(
'Missing Matrix registration shared secret. Start Synapse first or set MATRIX_REGISTRATION_SHARED_SECRET.',
);
}

return secret;
}

export const matrixRegistrationSecret = getMatrixRegistrationSecret();
export const testCreatePrerenderAuth =
buildCreatePrerenderAuth(realmSecretSeed);

Expand Down
4 changes: 4 additions & 0 deletions packages/realm-server/worker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ const shutdown = (onShutdown?: () => void) => {

process.on('SIGINT', () => shutdown());
process.on('SIGTERM', () => shutdown());
process.on('disconnect', () => {
log.info(`Parent IPC disconnected, shutting down worker manager...`);
shutdown();
});
process.on('uncaughtException', (err) => {
log.error(`Uncaught exception in worker manager:`, err);
shutdown();
Expand Down
1 change: 1 addition & 0 deletions packages/realm-server/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ let autoMigrate = migrateDB || undefined;
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.on('disconnect', shutdown);
process.on('message', (message) => {
if (message === 'stop') {
shutdown(); // warning this is async
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: boxel-development
description: Use when working on Boxel card development, especially creating or editing `.gts` card definitions, `.json` card instances, Boxel commands, themes, queries, templates, or related Boxel patterns in a synced workspace. Read the targeted files in `references/` instead of loading broad guidance by default.
---

# Boxel Development

Use this skill for Boxel card and app development. Keep the top-level guidance lean and load only the references needed for the task.

## Core Workflow

1. Confirm whether the task is about a card definition, card instance, query, command, theme, file asset, or styling.
2. Read only the specific reference files that match the task.
3. For file placement, naming, or `adoptsFrom.module` paths, also read `../boxel-file-structure/SKILL.md`.
4. Apply the rules from the relevant references exactly when they are marked critical.
5. Ignore Boxel in-app editor instructions unless you are explicitly operating inside that environment. In this repo, prefer normal filesystem edits and CLI workflows.

## Always Load First

- `references/dev-core-concept.md`
- `references/dev-technical-rules.md`
- `references/dev-quick-reference.md`

These three files establish the data model, the `contains` vs `linksTo` rule, required formats, inherited fields, and common import patterns.

## Load By Task

- Card structure and safe patterns:
`references/dev-core-patterns.md`
- Templates, delegated rendering, and field access:
`references/dev-template-patterns.md`
`references/dev-delegated-rendering.md`
- Styling and themes:
`references/dev-theme-design-system.md`
`references/dev-styling-design.md`
`references/dev-fitted-formats.md`
- Queries and data linking:
`references/dev-query-systems.md`
`references/dev-data-management.md`
- File-backed content and file asset cards:
`references/dev-file-def.md`
- Enum fields:
`references/dev-enumerations.md`
- Defensive component logic:
`references/dev-defensive-programming.md`
- Third-party libraries:
`references/dev-external-libraries.md`
- Command implementation:
`references/dev-command-development.md`
- Spec usage:
`references/dev-spec-usage.md`
- Replicate integration:
`references/dev-replicate-ai.md`

## Usually Ignore Unless Explicitly Relevant

- `references/dev-file-editing.md`
This is primarily for Boxel's in-app AI editing flow, not normal terminal-based editing.

## Key Reminders

- `CardDef` and `FileDef` references use `linksTo` / `linksToMany`.
- `FieldDef` values use `contains` / `containsMany`.
- Modern cards should implement `isolated`, `embedded`, and `fitted`.
- Be precise with relative JSON module paths.
- Prefer loading one or two reference files over reading the whole reference set.
Loading
Loading