test: Add unit tests for nodemon path configuration and enhance error handling

- Introduced a new test file to validate nodemon path configurations, ensuring they anchor to the package root by default.
- Enhanced the NodemonService to throw an error when nodemon exits immediately after starting, improving robustness.
- Added tests to cover scenarios where nodemon fails to start, ensuring proper logging and resource cleanup.
This commit is contained in:
Eli Bosley
2025-11-21 17:23:16 -05:00
parent b35da13234
commit d4f90d6d64
3 changed files with 70 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
describe('nodemon path configuration', () => {
const originalUnraidApiCwd = process.env.UNRAID_API_CWD;
beforeEach(() => {
vi.resetModules();
delete process.env.UNRAID_API_CWD;
});
afterEach(() => {
if (originalUnraidApiCwd === undefined) {
delete process.env.UNRAID_API_CWD;
} else {
process.env.UNRAID_API_CWD = originalUnraidApiCwd;
}
});
it('anchors nodemon paths to the package root by default', async () => {
const environment = await import('@app/environment.js');
const { UNRAID_API_ROOT, NODEMON_CONFIG_PATH, NODEMON_PATH, UNRAID_API_CWD } = environment;
expect(UNRAID_API_CWD).toBe(UNRAID_API_ROOT);
expect(NODEMON_CONFIG_PATH).toBe(join(UNRAID_API_ROOT, 'nodemon.json'));
expect(NODEMON_PATH).toBe(join(UNRAID_API_ROOT, 'node_modules', 'nodemon', 'bin', 'nodemon.js'));
});
});

View File

@@ -39,6 +39,7 @@ describe('NodemonService', () => {
const mockMkdir = vi.mocked(fs.mkdir);
const mockWriteFile = vi.mocked(fs.writeFile);
const mockRm = vi.mocked(fs.rm);
const killSpy = vi.spyOn(process, 'kill');
beforeEach(() => {
vi.clearAllMocks();
@@ -46,6 +47,7 @@ describe('NodemonService', () => {
mockWriteFile.mockResolvedValue(undefined as unknown as void);
mockRm.mockResolvedValue(undefined as unknown as void);
vi.mocked(fileExists).mockResolvedValue(false);
killSpy.mockReturnValue(true as unknown as boolean);
});
it('ensures directories needed by nodemon exist', async () => {
@@ -81,6 +83,7 @@ describe('NodemonService', () => {
stderr,
unref,
} as unknown as ReturnType<typeof execa>);
killSpy.mockReturnValue(true as unknown as boolean);
await service.start({ env: { LOG_LEVEL: 'DEBUG' } });
@@ -156,6 +159,32 @@ describe('NodemonService', () => {
expect(logger.info).not.toHaveBeenCalled();
});
it('throws when nodemon exits immediately after start', async () => {
const service = new NodemonService(logger);
const logStream = { pipe: vi.fn(), close: vi.fn() };
vi.mocked(createWriteStream).mockReturnValue(
logStream as unknown as ReturnType<typeof createWriteStream>
);
const stdout = { pipe: vi.fn() };
const stderr = { pipe: vi.fn() };
const unref = vi.fn();
vi.mocked(execa).mockReturnValue({
pid: 456,
stdout,
stderr,
unref,
} as unknown as ReturnType<typeof execa>);
killSpy.mockImplementation(() => {
throw new Error('not running');
});
const logsSpy = vi.spyOn(service, 'logs').mockResolvedValue('recent log lines');
await expect(service.start()).rejects.toThrow(/Nodemon exited immediately/);
expect(logStream.close).toHaveBeenCalled();
expect(mockRm).toHaveBeenCalledWith('/var/run/unraid-api/nodemon.pid', { force: true });
expect(logsSpy).toHaveBeenCalledWith(50);
});
it('returns not running when pid file is missing', async () => {
const service = new NodemonService(logger);
vi.mocked(fileExists).mockResolvedValue(false);

View File

@@ -89,6 +89,18 @@ export class NodemonService {
}
await writeFile(NODEMON_PID_PATH, `${nodemonProcess.pid}`);
// Give nodemon a brief moment to boot, then verify it is still alive.
await new Promise((resolve) => setTimeout(resolve, 200));
const stillRunning = await this.isPidRunning(nodemonProcess.pid);
if (!stillRunning) {
const recentLogs = await this.logs(50);
await rm(NODEMON_PID_PATH, { force: true });
logStream.close();
const logMessage = recentLogs ? ` Recent logs:\n${recentLogs}` : '';
throw new Error(`Nodemon exited immediately after start.${logMessage}`);
}
this.logger.info(`Started nodemon (pid ${nodemonProcess.pid})`);
} catch (error) {
logStream.close();