oom issue

This commit is contained in:
2025-12-22 13:22:21 -08:00
parent 22513a967b
commit 5b8f309ad8
5 changed files with 170 additions and 190 deletions

View File

@@ -50,177 +50,84 @@ describe('System Routes (/api/system)', () => {
});
describe('GET /pm2-status', () => {
it('should return success: true when pm2 process is online', async () => {
// Arrange: Simulate a successful `pm2 describe` output for an online process.
const pm2OnlineOutput = `
// Helper function to set up the mock for `child_process.exec` for each test case.
// This avoids repeating the complex mock implementation in every test.
const setupExecMock = (error: ExecException | null, stdout: string, stderr: string) => {
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) actualCallback(error, stdout, stderr);
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
};
const testCases = [
{
description: 'should return success: true when pm2 process is online',
mock: {
error: null,
stdout: `
┌─ PM2 info ────────────────┐
│ status │ online │
└───────────┴───────────┘
`;
type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void;
// A robust mock for `exec` that handles its multiple overloads.
// This avoids the complex and error-prone `...args` signature.
vi.mocked(exec).mockImplementation(
(
command: string,
options?: ExecOptions | ExecCallback | null,
callback?: ExecCallback | null,
) => {
// The actual callback can be the second or third argument.
const actualCallback = (
typeof options === 'function' ? options : callback
) as ExecCallback;
if (actualCallback) {
actualCallback(null, pm2OnlineOutput, '');
}
// Return a minimal object that satisfies the ChildProcess type for .unref()
return { unref: () => {} } as ReturnType<typeof exec>;
`,
stderr: '',
},
);
expectedStatus: 200,
expectedBody: { success: true, message: 'Application is online and running under PM2.' },
},
{
description: 'should return success: false when pm2 process is stopped',
mock: { error: null, stdout: '│ status │ stopped │', stderr: '' },
expectedStatus: 200,
expectedBody: { success: false, message: 'Application process exists but is not online.' },
},
{
description: 'should return success: false when pm2 process does not exist',
mock: {
error: Object.assign(new Error('Command failed'), { code: 1 }),
stdout: "[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist",
stderr: '',
},
expectedStatus: 200,
expectedBody: { success: false, message: 'Application process is not running under PM2.' },
},
{
description: 'should return 500 if pm2 command produces stderr output',
mock: { error: null, stdout: 'Some stdout', stderr: 'A non-fatal warning occurred.' },
expectedStatus: 500,
expectedBody: { message: 'PM2 command produced an error: A non-fatal warning occurred.' },
},
{
description: 'should return 500 on a generic exec error',
mock: { error: new Error('System error'), stdout: '', stderr: 'stderr output' },
expectedStatus: 500,
expectedBody: { message: 'System error' },
},
];
it.each(testCases)('$description', async ({ mock, expectedStatus, expectedBody }) => {
// Arrange
setupExecMock(mock.error as ExecException | null, mock.stdout, mock.stderr);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: true,
message: 'Application is online and running under PM2.',
});
});
it('should return success: false when pm2 process is stopped or errored', async () => {
const pm2StoppedOutput = `│ status │ stopped │`;
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(null, pm2StoppedOutput, '');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body.success).toBe(false);
});
it('should return success: false when pm2 process does not exist', async () => {
// Arrange: Simulate `pm2 describe` failing because the process isn't found.
const processNotFoundOutput =
"[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist";
const processNotFoundError = new Error(
'Command failed: pm2 describe flyer-crawler-api',
) as ExecException;
processNotFoundError.code = 1;
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(processNotFoundError, processNotFoundOutput, '');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: false,
message: 'Application process is not running under PM2.',
});
});
it('should return 500 if pm2 command produces stderr output', async () => {
// Arrange: Simulate a successful exit code but with content in stderr.
const stderrOutput = 'A non-fatal warning occurred.';
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(null, 'Some stdout', stderrOutput);
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
const response = await supertest(app).get('/api/system/pm2-status');
expect(response.status).toBe(500);
expect(response.body.message).toBe(`PM2 command produced an error: ${stderrOutput}`);
});
it('should return 500 on a generic exec error', async () => {
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(new Error('System error') as ExecException, '', 'stderr output');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toBe('System error');
expect(response.status).toBe(expectedStatus);
expect(response.body).toEqual(expectedBody);
});
});

View File

@@ -31,10 +31,14 @@ router.get(
(req: Request, res: Response, next: NextFunction) => {
// The name 'flyer-crawler-api' comes from your ecosystem.config.cjs file.
exec('pm2 describe flyer-crawler-api', (error, stdout, stderr) => {
// The "doesn't exist" message can appear in stdout or stderr depending on PM2 version and context.
const processNotFound =
stdout?.includes("doesn't exist") || stderr?.includes("doesn't exist");
if (error) {
// 'pm2 describe' exits with an error if the process is not found.
// We can treat this as a "fail" status for our check.
if (stdout && stdout.includes("doesn't exist")) {
if (processNotFound) {
logger.warn('[API /pm2-status] PM2 process "flyer-crawler-api" not found.');
return res.json({
success: false,