fix(cursor): add resolve guard, port validation, and daemon tests

- Add double-resolve guard in startDaemon with safeResolve wrapper
- Add port validation (1-65535) before Node.js script interpolation
- Fix misleading comment in stopDaemon (no PID file handling)
- Add getDaemonStatus test for no daemon running case
- Add stopDaemon tests for graceful non-existent PID handling
This commit is contained in:
Tam Nhu Tran
2026-02-12 04:27:35 +07:00
parent afb5e746b3
commit 7d4e6d6b65
2 changed files with 63 additions and 13 deletions
+24 -13
View File
@@ -140,10 +140,25 @@ export async function startDaemon(
return { success: true, pid: getPidFromFile() ?? undefined }; return { success: true, pid: getPidFromFile() ?? undefined };
} }
// Validate port before interpolation (prevents injection)
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
return { success: false, error: `Invalid port: ${config.port}` };
}
// For now, create a simple structure that will be filled in later // For now, create a simple structure that will be filled in later
// The actual server implementation will be added in a separate task // The actual server implementation will be added in a separate task
return new Promise((resolve) => { return new Promise((resolve) => {
let proc: ChildProcess; let proc: ChildProcess;
let resolved = false;
const safeResolve = (result: { success: boolean; pid?: number; error?: string }) => {
if (resolved) return;
resolved = true;
if (checkInterval) clearInterval(checkInterval);
resolve(result);
};
let checkInterval: NodeJS.Timeout | null = null;
try { try {
// Spawn a placeholder Node.js process // Spawn a placeholder Node.js process
@@ -184,15 +199,13 @@ export async function startDaemon(
// Wait for daemon to be ready (poll for up to 30 seconds) // Wait for daemon to be ready (poll for up to 30 seconds)
let attempts = 0; let attempts = 0;
const maxAttempts = 30; const maxAttempts = 30;
const checkInterval = setInterval(async () => { checkInterval = setInterval(async () => {
attempts++; attempts++;
if (await isDaemonRunning(config.port)) { if (await isDaemonRunning(config.port)) {
clearInterval(checkInterval); safeResolve({ success: true, pid: proc.pid });
resolve({ success: true, pid: proc.pid });
} else if (attempts >= maxAttempts) { } else if (attempts >= maxAttempts) {
clearInterval(checkInterval); safeResolve({
resolve({
success: false, success: false,
error: 'Daemon did not start within 30 seconds', error: 'Daemon did not start within 30 seconds',
}); });
@@ -200,34 +213,32 @@ export async function startDaemon(
}, 1000); }, 1000);
proc.on('error', (err) => { proc.on('error', (err) => {
clearInterval(checkInterval); safeResolve({
resolve({
success: false, success: false,
error: `Failed to start daemon: ${err.message}`, error: `Failed to start daemon: ${err.message}`,
}); });
}); });
proc.on('exit', (code, signal) => { proc.on('exit', (code, signal) => {
clearInterval(checkInterval);
if (code === null) { if (code === null) {
resolve({ safeResolve({
success: false, success: false,
error: `Daemon process was killed by signal ${signal}`, error: `Daemon process was killed by signal ${signal}`,
}); });
} else if (code === 0) { } else if (code === 0) {
resolve({ safeResolve({
success: false, success: false,
error: 'Daemon process exited unexpectedly with code 0', error: 'Daemon process exited unexpectedly with code 0',
}); });
} else if (code !== null) { } else if (code !== null) {
resolve({ safeResolve({
success: false, success: false,
error: `Daemon process exited with code ${code}`, error: `Daemon process exited with code ${code}`,
}); });
} }
}); });
} catch (err) { } catch (err) {
resolve({ safeResolve({
success: false, success: false,
error: `Failed to spawn daemon: ${(err as Error).message}`, error: `Failed to spawn daemon: ${(err as Error).message}`,
}); });
@@ -242,7 +253,7 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string }
const pid = getPidFromFile(); const pid = getPidFromFile();
if (!pid) { if (!pid) {
// No PID file, try to find by port // No PID file — daemon is not running or was already stopped
removePidFile(); removePidFile();
return { success: true }; return { success: true };
} }
+39
View File
@@ -11,6 +11,8 @@ import {
writePidToFile, writePidToFile,
removePidFile, removePidFile,
isDaemonRunning, isDaemonRunning,
getDaemonStatus,
stopDaemon,
} from '../../../src/cursor/cursor-daemon'; } from '../../../src/cursor/cursor-daemon';
// Test isolation // Test isolation
@@ -121,3 +123,40 @@ describe('isDaemonRunning', () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
}); });
describe('getDaemonStatus', () => {
it('returns status with running=false when no daemon running', async () => {
const status = await getDaemonStatus(19999);
expect(status.running).toBe(false);
expect(status.port).toBe(19999);
expect(status.pid).toBeUndefined();
});
it('returns status with pid when PID file exists but daemon not running', async () => {
writePidToFile(99999);
const status = await getDaemonStatus(19999);
expect(status.running).toBe(false);
expect(status.port).toBe(19999);
expect(status.pid).toBeUndefined();
});
});
describe('stopDaemon', () => {
it('returns success when no PID file exists', async () => {
const result = await stopDaemon();
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
it('returns success when PID refers to non-existent process', async () => {
// Write a PID that doesn't exist
writePidToFile(999999);
const result = await stopDaemon();
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
// PID file should be removed
const pidFile = path.join(ccsDir(), 'cursor', 'daemon.pid');
expect(fs.existsSync(pidFile)).toBe(false);
});
});