test: Add 8 people voice tests
This commit is contained in:
132
e2e/helpers/test-server.ts
Normal file
132
e2e/helpers/test-server.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { createServer } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export interface TestServerHandle {
|
||||
port: number;
|
||||
url: string;
|
||||
stop: () => Promise<void>;
|
||||
}
|
||||
|
||||
const E2E_DIR = join(__dirname, '..');
|
||||
const START_SERVER_SCRIPT = join(E2E_DIR, 'helpers', 'start-test-server.js');
|
||||
|
||||
export async function startTestServer(retries = 3): Promise<TestServerHandle> {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
const port = await allocatePort();
|
||||
const child = spawn(process.execPath, [START_SERVER_SCRIPT], {
|
||||
cwd: E2E_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
TEST_SERVER_PORT: String(port)
|
||||
},
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (chunk: Buffer | string) => {
|
||||
process.stdout.write(chunk.toString());
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (chunk: Buffer | string) => {
|
||||
process.stderr.write(chunk.toString());
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServerReady(port, child);
|
||||
} catch (error) {
|
||||
await stopServer(child);
|
||||
|
||||
if (attempt < retries) {
|
||||
console.log(`[E2E Server] Attempt ${attempt} failed, retrying...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
port,
|
||||
url: `http://localhost:${port}`,
|
||||
stop: async () => {
|
||||
await stopServer(child);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('startTestServer: unreachable');
|
||||
}
|
||||
|
||||
async function allocatePort(): Promise<number> {
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
const probe = createServer();
|
||||
|
||||
probe.once('error', reject);
|
||||
probe.listen(0, '127.0.0.1', () => {
|
||||
const address = probe.address();
|
||||
|
||||
if (!address || typeof address === 'string') {
|
||||
probe.close();
|
||||
reject(new Error('Failed to resolve an ephemeral test server port'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { port } = address;
|
||||
|
||||
probe.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(port);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServerReady(port: number, child: ChildProcess, timeoutMs = 30_000): Promise<void> {
|
||||
const readyUrl = `http://127.0.0.1:${port}/api/servers?limit=1`;
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error(`Test server exited before becoming ready (exit code ${child.exitCode})`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(readyUrl);
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Server still starting.
|
||||
}
|
||||
|
||||
await wait(250);
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for test server on port ${port}`);
|
||||
}
|
||||
|
||||
async function stopServer(child: ChildProcess): Promise<void> {
|
||||
if (child.exitCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
child.kill('SIGTERM');
|
||||
|
||||
const exited = await Promise.race([once(child, 'exit').then(() => true), wait(3_000).then(() => false)]);
|
||||
|
||||
if (!exited && child.exitCode === null) {
|
||||
child.kill('SIGKILL');
|
||||
await once(child, 'exit');
|
||||
}
|
||||
}
|
||||
|
||||
function wait(durationMs: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, durationMs);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user