Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion packages/software-factory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"test:all": "NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/test.ts",
"test:node": "NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/test.ts --node-only",
"test:playwright": "playwright test",
"test:playwright-e2e": "playwright test --config playwright.e2e.config.ts",
"test:playwright:headed": "playwright test --headed",
"test:realm": "NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/run-realm-tests.ts"
},
Expand Down
23 changes: 18 additions & 5 deletions packages/software-factory/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { defineConfig } from '@playwright/test';

import { sharedConfig } from './playwright.shared';
const realmPort = Number(process.env.SOFTWARE_FACTORY_REALM_PORT ?? 4205);
const realmURL =
process.env.SOFTWARE_FACTORY_REALM_URL ??
`http://localhost:${realmPort}/test/`;

export default defineConfig({
...sharedConfig,
testDir: './tests',
testMatch: ['**/*.spec.ts'],
// factory-target-realm.spec.ts is excluded here and run separately
// via `pnpm test:playwright-e2e` (see CS-10472 for context)
testIgnore: ['**/factory-target-realm.spec.ts'],
fullyParallel: false,
reporter: process.env.CI ? [['list']] : undefined,
workers: 1,
timeout: 60_000,
expect: {
timeout: 15_000,
},
use: {
baseURL: realmURL,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
globalSetup: './playwright.global-setup.ts',
globalTeardown: './playwright.global-teardown.ts',
});
9 changes: 0 additions & 9 deletions packages/software-factory/playwright.e2e.config.ts

This file was deleted.

24 changes: 15 additions & 9 deletions packages/software-factory/playwright.global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './src/runtime-metadata';

const packageRoot = resolve(__dirname);
const tsNodeBin = resolve(packageRoot, 'node_modules', '.bin', 'ts-node');
const configuredRealmDir = resolve(
packageRoot,
process.env.SOFTWARE_FACTORY_REALM_DIR ?? 'test-fixtures/darkfactory-adopter',
Expand Down Expand Up @@ -155,16 +156,21 @@ export default async function globalSetup() {

supportLog.debug(`starting serve:support for realm ${realmDir}`);
let logs = '';
let child = spawn('pnpm', ['serve:support', realmDir], {
cwd: packageRoot,
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
SOFTWARE_FACTORY_SUPPORT_METADATA_FILE: metadataFile,
SOFTWARE_FACTORY_SOURCE_REALM_DIR: testSourceRealmDir,
let child = spawn(
tsNodeBin,
['--transpileOnly', 'src/cli/serve-support.ts', realmDir],
{
cwd: packageRoot,
detached: true,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
NODE_NO_WARNINGS: '1',
SOFTWARE_FACTORY_SUPPORT_METADATA_FILE: metadataFile,
SOFTWARE_FACTORY_SOURCE_REALM_DIR: testSourceRealmDir,
},
},
});
);

mirrorChildOutput(
child,
Expand Down
23 changes: 0 additions & 23 deletions packages/software-factory/playwright.shared.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/software-factory/src/cli/serve-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,22 @@ async function main(): Promise<void> {

console.log(JSON.stringify(payload, null, 2));

let cleanExit = false;
process.on('exit', () => {
if (!cleanExit) {
for (let pid of runtime.childPids) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// already dead
}
}
}
});

let stop = async () => {
await runtime.stop();
cleanExit = true;
process.exit(0);
};

Expand Down
4 changes: 4 additions & 0 deletions packages/software-factory/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface StartedFactoryRealm {
realmDir: string;
realmURL: URL;
databaseName: string;
childPids: number[];
cardURL(path: string): string;
createBearerToken(user?: string, permissions?: RealmAction[]): string;
authorizationHeaders(
Expand Down Expand Up @@ -1634,6 +1635,9 @@ export async function startFactoryRealmServer(
realmDir,
realmURL,
databaseName,
childPids: [stack.realmServer.pid, stack.workerManager.pid].filter(
(pid): pid is number => pid != null,
),
cardURL(path: string) {
return new URL(path, realmURL).href;
},
Expand Down
6 changes: 0 additions & 6 deletions packages/software-factory/tests/factory-target-realm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ test.use({ realmDir: bootstrapTargetDir });
test.use({ realmServerMode: 'isolated' });
test.setTimeout(180_000);

// Known issue (CS-10472): This test hangs when run in the same Playwright
// suite after other specs that start isolated realm servers. The subprocess's
// auth middleware hangs during Matrix auth → realm _session when prior
// isolated realm server teardowns leave orphaned processes. Passes reliably
// when run in isolation:
// pnpm exec playwright test tests/factory-target-realm.spec.ts
test('factory:go creates a target realm and bootstraps project artifacts end-to-end', async ({
realm,
}) => {
Expand Down
93 changes: 73 additions & 20 deletions packages/software-factory/tests/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from 'node:child_process';
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';

Expand Down Expand Up @@ -41,11 +42,18 @@ type SharedRealmHandle = {
};

const packageRoot = resolve(process.cwd());
const tsNodeBin = resolve(packageRoot, 'node_modules', '.bin', 'ts-node');
const defaultRealmDir = resolve(
packageRoot,
process.env.SOFTWARE_FACTORY_REALM_DIR ?? 'test-fixtures/darkfactory-adopter',
);
const realmPort = Number(process.env.SOFTWARE_FACTORY_REALM_PORT ?? 4205);
const compatPort = Number(
process.env.SOFTWARE_FACTORY_COMPAT_REALM_PORT ?? 4201,
);
const workerManagerPort = Number(
process.env.SOFTWARE_FACTORY_WORKER_MANAGER_PORT ?? 4232,
);
const localBasePrefix = `http://localhost:${realmPort}/base/`;
const localSkillsPrefix = `http://localhost:${realmPort}/skills/`;
const testSourceRealmDir = resolve(
Expand All @@ -70,6 +78,41 @@ function killProcessGroup(pid: number, signal: NodeJS.Signals) {
}
}

async function waitForPortFree(
port: number,
timeoutMs = 10_000,
): Promise<void> {
let startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
let free = await new Promise<boolean>((resolve, reject) => {
let server = createServer();
server.once('error', (error: NodeJS.ErrnoException) => {
server.close(() => {
if (error.code === 'EADDRINUSE') {
resolve(false);
} else {
reject(error);
}
});
});
server.listen(port, '127.0.0.1', () => {
server.close((closeError) => {
if (closeError) {
reject(closeError);
} else {
resolve(true);
}
});
});
});
if (free) {
return;
}
await new Promise((r) => setTimeout(r, 200));
}
throw new Error(`Port ${port} still in use after ${timeoutMs}ms`);
}

async function waitForMetadataFile<T>(
metadataFile: string,
child: ReturnType<typeof spawn>,
Expand Down Expand Up @@ -108,27 +151,32 @@ async function startRealmProcess(realmDir = defaultRealmDir) {
})
: undefined;

let child = spawn('pnpm', ['serve:realm', realmDir], {
cwd: packageRoot,
detached: true,
env: {
...process.env,
SOFTWARE_FACTORY_METADATA_FILE: metadataFile,
SOFTWARE_FACTORY_SOURCE_REALM_DIR: testSourceRealmDir,
...(supportMetadata?.context
? {
SOFTWARE_FACTORY_CONTEXT: JSON.stringify(supportMetadata.context),
}
: {}),
...(supportMetadata?.templateDatabaseName
? {
SOFTWARE_FACTORY_TEMPLATE_DATABASE_NAME:
supportMetadata.templateDatabaseName,
}
: {}),
let child = spawn(
tsNodeBin,
['--transpileOnly', 'src/cli/serve-realm.ts', realmDir],
{
cwd: packageRoot,
detached: true,
env: {
...process.env,
NODE_NO_WARNINGS: '1',
SOFTWARE_FACTORY_METADATA_FILE: metadataFile,
SOFTWARE_FACTORY_SOURCE_REALM_DIR: testSourceRealmDir,
...(supportMetadata?.context
? {
SOFTWARE_FACTORY_CONTEXT: JSON.stringify(supportMetadata.context),
}
: {}),
...(supportMetadata?.templateDatabaseName
? {
SOFTWARE_FACTORY_TEMPLATE_DATABASE_NAME:
supportMetadata.templateDatabaseName,
}
: {}),
},
stdio: ['ignore', 'pipe', 'pipe'],
},
stdio: ['ignore', 'pipe', 'pipe'],
});
);

child.stdout?.on('data', (chunk) => {
logs = appendLog(logs, String(chunk));
Expand Down Expand Up @@ -175,6 +223,11 @@ async function startRealmProcess(realmDir = defaultRealmDir) {
});
});
}
await Promise.all([
waitForPortFree(realmPort),
waitForPortFree(compatPort),
waitForPortFree(workerManagerPort),
]);
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
Expand Down
Loading