website refactor
This commit is contained in:
100
tests/integration/harness/ApiServerHarness.ts
Normal file
100
tests/integration/harness/ApiServerHarness.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface ApiServerHarnessOptions {
|
||||
port?: number;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export class ApiServerHarness {
|
||||
private process: ChildProcess | null = null;
|
||||
private logs: string[] = [];
|
||||
private port: number;
|
||||
|
||||
constructor(options: ApiServerHarnessOptions = {}) {
|
||||
this.port = options.port || 3001;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cwd = join(process.cwd(), 'apps/api');
|
||||
|
||||
this.process = spawn('npm', ['run', 'start:dev'], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: this.port.toString(),
|
||||
GRIDPILOT_API_PERSISTENCE: 'inmemory',
|
||||
ENABLE_BOOTSTRAP: 'true',
|
||||
},
|
||||
shell: true,
|
||||
detached: true,
|
||||
});
|
||||
|
||||
let resolved = false;
|
||||
|
||||
const checkReadiness = async () => {
|
||||
if (resolved) return;
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${this.port}/health`);
|
||||
if (res.ok) {
|
||||
resolved = true;
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
// Not ready yet
|
||||
}
|
||||
};
|
||||
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
this.logs.push(str);
|
||||
if (str.includes('Nest application successfully started') || str.includes('started')) {
|
||||
checkReadiness();
|
||||
}
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
this.logs.push(str);
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
if (!resolved && code !== 0 && code !== null) {
|
||||
resolved = true;
|
||||
reject(new Error(`API server exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 60 seconds
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(new Error(`API server failed to start within 60s. Logs:\n${this.getLogTail(20)}`));
|
||||
}
|
||||
}, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.process && this.process.pid) {
|
||||
try {
|
||||
process.kill(-this.process.pid);
|
||||
} catch (e) {
|
||||
this.process.kill();
|
||||
}
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
|
||||
getLogTail(lines: number = 60): string {
|
||||
return this.logs.slice(-lines).join('');
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ export class WebsiteServerHarness {
|
||||
private process: ChildProcess | null = null;
|
||||
private logs: string[] = [];
|
||||
private port: number;
|
||||
private options: WebsiteServerHarnessOptions;
|
||||
|
||||
constructor(options: WebsiteServerHarnessOptions = {}) {
|
||||
this.options = options;
|
||||
this.port = options.port || 3000;
|
||||
}
|
||||
|
||||
@@ -28,46 +30,75 @@ export class WebsiteServerHarness {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...this.options.env,
|
||||
PORT: this.port.toString(),
|
||||
...((this.process as unknown as { env: Record<string, string> })?.env || {}),
|
||||
},
|
||||
shell: true,
|
||||
detached: true, // Start in a new process group
|
||||
});
|
||||
|
||||
let resolved = false;
|
||||
|
||||
const checkReadiness = async () => {
|
||||
if (resolved) return;
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${this.port}`, { method: 'HEAD' });
|
||||
if (res.ok || res.status === 307 || res.status === 200) {
|
||||
resolved = true;
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
// Not ready yet
|
||||
}
|
||||
};
|
||||
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
this.logs.push(str);
|
||||
if (str.includes('ready') || str.includes('started') || str.includes('Local:')) {
|
||||
resolve();
|
||||
checkReadiness();
|
||||
}
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
this.logs.push(str);
|
||||
console.error(`[Website Server Error] ${str}`);
|
||||
// Don't console.error here as it might be noisy, but keep in logs
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`Website server exited with code ${code}`);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 30 seconds
|
||||
this.process.on('exit', (code) => {
|
||||
if (!resolved && code !== 0 && code !== null) {
|
||||
resolved = true;
|
||||
reject(new Error(`Website server exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout after 60 seconds (Next.js dev can be slow)
|
||||
setTimeout(() => {
|
||||
reject(new Error('Website server failed to start within 30s'));
|
||||
}, 30000);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
reject(new Error(`Website server failed to start within 60s. Logs:\n${this.getLogTail(20)}`));
|
||||
}
|
||||
}, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.process) {
|
||||
this.process.kill();
|
||||
if (this.process && this.process.pid) {
|
||||
try {
|
||||
// Kill the process group since we used detached: true
|
||||
process.kill(-this.process.pid);
|
||||
} catch (e) {
|
||||
// Fallback to normal kill
|
||||
this.process.kill();
|
||||
}
|
||||
this.process = null;
|
||||
}
|
||||
}
|
||||
@@ -84,8 +115,10 @@ export class WebsiteServerHarness {
|
||||
const errorPatterns = [
|
||||
'uncaughtException',
|
||||
'unhandledRejection',
|
||||
'Error: ',
|
||||
// 'Error: ', // Too broad, catches expected API errors
|
||||
];
|
||||
|
||||
// Only fail on actual process-level errors or unexpected server crashes
|
||||
return this.logs.some(log => errorPatterns.some(pattern => log.includes(pattern)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user