/** * Tests for ProcessManager utilities */ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { isProcessAlive, readPidFile, writePidFile, removePidFile, collectProcessesToTerminate } from '../ProcessManager'; describe('ProcessManager', () => { describe('isProcessAlive', () => { it('should return true for current process', () => { expect(isProcessAlive(process.pid)).toBe(true); }); it('should return false for non-existent process', () => { // Use a very high PID that is unlikely to exist expect(isProcessAlive(999999)).toBe(false); }); }); describe('PID file management', () => { let tmpDir: string; let pidFilePath: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'process-manager-test-')); pidFilePath = path.join(tmpDir, 'test.pid'); }); afterEach(() => { if (fs.existsSync(tmpDir)) { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); it('should write and read PID file', () => { const testPid = 12345; writePidFile(pidFilePath, testPid); expect(fs.existsSync(pidFilePath)).toBe(true); const readPid = readPidFile(pidFilePath); expect(readPid).toBe(testPid); }); it('should return undefined for non-existent PID file', () => { const readPid = readPidFile(path.join(tmpDir, 'nonexistent.pid')); expect(readPid).toBeUndefined(); }); it('should remove PID file', () => { const testPid = 12345; writePidFile(pidFilePath, testPid); expect(fs.existsSync(pidFilePath)).toBe(true); removePidFile(pidFilePath); expect(fs.existsSync(pidFilePath)).toBe(false); }); it('should handle removing non-existent PID file gracefully', () => { expect(() => removePidFile(path.join(tmpDir, 'nonexistent.pid'))).not.toThrow(); }); it('should return undefined for invalid PID content', () => { fs.writeFileSync(pidFilePath, 'not-a-number', 'utf8'); const readPid = readPidFile(pidFilePath); expect(readPid).toBeUndefined(); }); }); describe('collectProcessesToTerminate', () => { it('should walk through shell parents to reach npm runners', () => { const parentMap = new Map([ [3000, { ppid: 4000, command: 'node dist/server.js' }], [4000, { ppid: 5000, command: 'node --watch src/server.ts' }], [5000, { ppid: 6000, command: 'sh' }], [6000, { ppid: 7000, command: 'npm run dev' }], [7000, { ppid: null, command: 'node scripts/run-local-server.ts' }] ]); const resolver = (pid: number): { ppid: number | null; command: string | null } | null => parentMap.get(pid) ?? null; const result = collectProcessesToTerminate([3000], resolver); expect(new Set(result)).toEqual(new Set([3000, 4000, 6000, 7000])); }); it('should return empty array for empty input', () => { const resolver = (): null => null; const result = collectProcessesToTerminate([], resolver); expect(result).toEqual([]); }); it('should handle single process with no parent', () => { const resolver = (): { ppid: number | null; command: string | null } => ({ ppid: null, command: 'node server.js' }); const result = collectProcessesToTerminate([1000], resolver); expect(result).toEqual([1000]); }); it('should not include non-node-related parents', () => { const parentMap = new Map([ [3000, { ppid: 4000, command: 'node server.js' }], [4000, { ppid: 5000, command: 'bash' }], [5000, { ppid: null, command: 'systemd' }] ]); const resolver = (pid: number): { ppid: number | null; command: string | null } | null => parentMap.get(pid) ?? null; const result = collectProcessesToTerminate([3000], resolver); // Should only include node process, not bash or systemd expect(result).toEqual([3000]); }); it('should handle circular references', () => { const parentMap = new Map([ [3000, { ppid: 4000, command: 'node' }], [4000, { ppid: 3000, command: 'npm' }] // Circular reference ]); const resolver = (pid: number): { ppid: number | null; command: string | null } | null => parentMap.get(pid) ?? null; // Should not hang due to circular reference const result = collectProcessesToTerminate([3000], resolver); expect(result.length).toBeGreaterThan(0); }); }); });