Skip to content

Commit c0832c6

Browse files
committed
fix: PM2 integration test cleanup
1 parent 5912cd2 commit c0832c6

4 files changed

Lines changed: 233 additions & 250 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { existsSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
import { execa } from 'execa';
6+
import pm2 from 'pm2';
7+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
8+
9+
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running.js';
10+
11+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
12+
const PROJECT_ROOT = join(__dirname, '../../../../..');
13+
const DUMMY_PROCESS_PATH = join(__dirname, 'dummy-process.js');
14+
const CLI_PATH = join(PROJECT_ROOT, 'dist/cli.js');
15+
const TEST_PROCESS_NAME = 'test-unraid-api';
16+
17+
// Shared PM2 connection state
18+
let pm2Connected = false;
19+
20+
// Helper function to run CLI command (assumes CLI is built)
21+
async function runCliCommand(command: string, options: any = {}) {
22+
return await execa('node', [CLI_PATH, command], options);
23+
}
24+
25+
// Helper to ensure PM2 connection is established
26+
async function ensurePM2Connection() {
27+
if (pm2Connected) return;
28+
29+
return new Promise<void>((resolve, reject) => {
30+
pm2.connect((err) => {
31+
if (err) {
32+
reject(err);
33+
return;
34+
}
35+
pm2Connected = true;
36+
resolve();
37+
});
38+
});
39+
}
40+
41+
// Helper to delete specific test processes (lightweight, reuses connection)
42+
async function deleteTestProcesses() {
43+
if (!pm2Connected) {
44+
// No connection, nothing to clean up
45+
return;
46+
}
47+
48+
const deletePromise = new Promise<void>((resolve) => {
49+
// Delete specific processes we might have created
50+
const processNames = ['unraid-api', TEST_PROCESS_NAME];
51+
let deletedCount = 0;
52+
53+
const deleteNext = () => {
54+
if (deletedCount >= processNames.length) {
55+
resolve();
56+
return;
57+
}
58+
59+
const processName = processNames[deletedCount];
60+
pm2.delete(processName, (deleteErr) => {
61+
// Ignore errors, process might not exist
62+
deletedCount++;
63+
deleteNext();
64+
});
65+
};
66+
67+
deleteNext();
68+
});
69+
70+
const timeoutPromise = new Promise<void>((resolve) => {
71+
setTimeout(() => resolve(), 3000); // 3 second timeout
72+
});
73+
74+
return Promise.race([deletePromise, timeoutPromise]);
75+
}
76+
77+
// Helper to ensure PM2 is completely clean (heavy cleanup with daemon kill)
78+
async function cleanupAllPM2Processes() {
79+
// First delete test processes if we have a connection
80+
if (pm2Connected) {
81+
await deleteTestProcesses();
82+
}
83+
84+
return new Promise<void>((resolve) => {
85+
// Always connect fresh for daemon kill (in case we weren't connected)
86+
pm2.connect((err) => {
87+
if (err) {
88+
// If we can't connect, assume PM2 is not running
89+
pm2Connected = false;
90+
resolve();
91+
return;
92+
}
93+
94+
// Kill the daemon to ensure fresh state
95+
pm2.killDaemon((killErr) => {
96+
pm2.disconnect();
97+
pm2Connected = false;
98+
// Small delay to let PM2 fully shutdown
99+
setTimeout(resolve, 500);
100+
});
101+
});
102+
});
103+
}
104+
105+
describe.skipIf(!!process.env.CI)('PM2 integration tests', () => {
106+
beforeAll(async () => {
107+
// Build the CLI if it doesn't exist (only for CLI tests)
108+
if (!existsSync(CLI_PATH)) {
109+
console.log('Building CLI for integration tests...');
110+
try {
111+
await execa('pnpm', ['build'], {
112+
cwd: PROJECT_ROOT,
113+
stdio: 'inherit',
114+
timeout: 120000, // 2 minute timeout for build
115+
});
116+
} catch (error) {
117+
console.error('Failed to build CLI:', error);
118+
throw new Error(
119+
'Cannot run CLI integration tests without built CLI. Run `pnpm build` first.'
120+
);
121+
}
122+
}
123+
124+
// Only do a full cleanup once at the beginning
125+
await cleanupAllPM2Processes();
126+
}, 150000); // 2.5 minute timeout for setup
127+
128+
afterAll(async () => {
129+
// Only do a full cleanup once at the end
130+
await cleanupAllPM2Processes();
131+
});
132+
133+
afterEach(async () => {
134+
// Lightweight cleanup after each test - just delete our test processes
135+
await deleteTestProcesses();
136+
}, 5000); // 5 second timeout for cleanup
137+
138+
describe('isUnraidApiRunning function', () => {
139+
it('should return false when PM2 is not running the unraid-api process', async () => {
140+
const result = await isUnraidApiRunning();
141+
expect(result).toBe(false);
142+
});
143+
144+
it('should return true when PM2 has unraid-api process running', async () => {
145+
// Ensure PM2 connection
146+
await ensurePM2Connection();
147+
148+
// Start a dummy process with the name 'unraid-api'
149+
await new Promise<void>((resolve, reject) => {
150+
pm2.start(
151+
{
152+
script: DUMMY_PROCESS_PATH,
153+
name: 'unraid-api',
154+
},
155+
(startErr) => {
156+
if (startErr) return reject(startErr);
157+
resolve();
158+
}
159+
);
160+
});
161+
162+
// Give PM2 time to start the process
163+
await new Promise((resolve) => setTimeout(resolve, 2000));
164+
165+
const result = await isUnraidApiRunning();
166+
expect(result).toBe(true);
167+
}, 30000);
168+
169+
it('should return false when unraid-api process is stopped', async () => {
170+
// Ensure PM2 connection
171+
await ensurePM2Connection();
172+
173+
// Start and then stop the process
174+
await new Promise<void>((resolve, reject) => {
175+
pm2.start(
176+
{
177+
script: DUMMY_PROCESS_PATH,
178+
name: 'unraid-api',
179+
},
180+
(startErr) => {
181+
if (startErr) return reject(startErr);
182+
183+
// Stop the process after starting
184+
setTimeout(() => {
185+
pm2.stop('unraid-api', (stopErr) => {
186+
if (stopErr) return reject(stopErr);
187+
resolve();
188+
});
189+
}, 1000);
190+
}
191+
);
192+
});
193+
194+
await new Promise((resolve) => setTimeout(resolve, 1000));
195+
196+
const result = await isUnraidApiRunning();
197+
expect(result).toBe(false);
198+
}, 30000);
199+
200+
it('should handle PM2 connection errors gracefully', async () => {
201+
// Set an invalid PM2_HOME to force connection failure
202+
const originalPM2Home = process.env.PM2_HOME;
203+
process.env.PM2_HOME = '/invalid/path/that/does/not/exist';
204+
205+
const result = await isUnraidApiRunning();
206+
expect(result).toBe(false);
207+
208+
// Restore original PM2_HOME
209+
if (originalPM2Home) {
210+
process.env.PM2_HOME = originalPM2Home;
211+
} else {
212+
delete process.env.PM2_HOME;
213+
}
214+
}, 15000); // 15 second timeout to allow for the Promise.race timeout
215+
});
216+
});

api/src/__test__/core/utils/pm2/unraid-api-running.test.ts

Lines changed: 0 additions & 102 deletions
This file was deleted.

0 commit comments

Comments
 (0)