fix(bar): harden lifecycle process handling

This commit is contained in:
Tam Nhu Tran committed 2026-08-08 21:17:51 -04:00
1 parent d27b53f235
commit 1a80717f99
11 files changed
+1064 -169

No files matched your search

+147 -10
View File
@@ -169,6 +169,13 @@ describe('bar command dispatcher (index.ts)', () => {
expect(calls).toEqual(['launch:']);
});
it('rejects an unknown bare launch flag without dispatching launch', async () => {
const handleBarCommand = await loadHandleBarCommand();
await handleBarCommand(['--porrt', '3999']);
expect(calls).toEqual([]);
expect(process.exitCode).toBe(1);
});
it('dispatches `ccs bar install` to install subcommand', async () => {
const handleBarCommand = await loadHandleBarCommand();
await handleBarCommand(['install']);
@@ -3040,7 +3047,7 @@ describe('defaultFindRunningServer: socket-level 401/403 classifies authRequired
// parser correctly sets authRequired without relying on the higher-level dep
// injection that existing tests use (which bypasses real status-line parsing).
function makeNetMock(statusLine: string) {
function makeNetMock(statusLine: string, token: string) {
return {
connect: (opts: { host: string; port: number }, onConnect: () => void): unknown => {
const listeners: Record<string, Array<(arg?: unknown) => void>> = {};
@@ -3054,19 +3061,25 @@ describe('defaultFindRunningServer: socket-level 401/403 classifies authRequired
void cb;
return socket;
},
write() {
write(request: string) {
const nonce =
request.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ??
'';
const proof = createBarAuthProof(token, nonce);
setImmediate(() => {
for (const cb of listeners.data ?? []) {
cb(
Buffer.from(`${statusLine}\r\n${BAR_AUTH_TOKEN_HEADER}: ${proof}\r\n\r\n`, 'utf8')
);
}
});
return true;
},
destroy() {
return socket;
},
};
setImmediate(() => {
onConnect();
for (const cb of listeners.data ?? []) {
cb(Buffer.from(`${statusLine}\r\n\r\n`, 'utf8'));
}
});
setImmediate(onConnect);
return socket;
},
};
@@ -3081,7 +3094,8 @@ describe('defaultFindRunningServer: socket-level 401/403 classifies authRequired
JSON.stringify({ port: 41401, baseUrl: 'http://127.0.0.1:41401', authMode: 'loopback' })
);
mock.module('net', () => makeNetMock('HTTP/1.1 401 Unauthorized'));
const token = getOrCreateBarAuthToken(ccsDir);
mock.module('net', () => makeNetMock('HTTP/1.1 401 Unauthorized', token));
moduleSeq++;
const { defaultFindRunningServer } = (await import(
@@ -3111,7 +3125,8 @@ describe('defaultFindRunningServer: socket-level 401/403 classifies authRequired
JSON.stringify({ port: 41403, baseUrl: 'http://127.0.0.1:41403', authMode: 'loopback' })
);
mock.module('net', () => makeNetMock('HTTP/1.1 403 Forbidden'));
const token = getOrCreateBarAuthToken(ccsDir);
mock.module('net', () => makeNetMock('HTTP/1.1 403 Forbidden', token));
moduleSeq++;
const { defaultFindRunningServer } = (await import(
@@ -3302,6 +3317,30 @@ describe('defaultWaitForServerLive: rogue 200 without matching token is rejected
expect(result === 'timeout' || result === 'rejected').toBe(true);
});
it('unrelated 401/403 without a CCS proof does not trigger auth-required identity', async () => {
for (const status of [401, 403]) {
mock.module('net', () =>
buildNetMock(`HTTP/1.1 ${status} Unrelated Service\r\nConnection: close\r\n\r\n`)
);
moduleSeq++;
const { defaultWaitForServerLive } = (await import(
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
)) as {
defaultWaitForServerLive: (baseUrl: string) => Promise<void>;
};
const result = await Promise.race([
defaultWaitForServerLive(`http://127.0.0.1:${9900 + status}`).then(
() => 'resolved' as const,
() => 'rejected' as const
),
new Promise<'still-probing'>((resolve) => setTimeout(() => resolve('still-probing'), 300)),
]);
expect(result).toBe('still-probing');
mock.restore();
}
});
it('correct-token 200 resolves defaultWaitForServerLive immediately', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
@@ -3798,6 +3837,104 @@ describe('launch: --port selects the server port', () => {
expect(barJson.port).toBe(3999);
});
it('preflights the destination before stopping a healthy running server', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps } = makePortDeps(ccsDir);
const events: string[] = [];
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.getPort = async () => {
events.push('preflight');
return 3999;
};
deps.stopDetachedServer = () => {
events.push('stop');
};
await handleBarLaunch(['--port', '3999'], deps);
expect(events).toEqual(['preflight', 'stop']);
});
it('leaves the healthy server running when destination preflight is busy', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps, seen } = makePortDeps(ccsDir);
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.getPort = async () => 4000;
await handleBarLaunch(['--port', '3999'], deps);
expect(seen.stopped).toBe(false);
expect(seen.spawnPort).toBeNull();
});
it('aborts the move when the verified stop fails', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps, seen } = makePortDeps(ccsDir);
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.stopDetachedServer = () => {
throw new Error('identity mismatch');
};
await handleBarLaunch(['--port', '3999'], deps);
expect(seen.spawnPort).toBeNull();
});
it('restores the prior server when a check-to-bind race breaks the replacement', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps } = makePortDeps(ccsDir);
const spawnedPorts: number[] = [];
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.spawnDetachedServer = (port: number) => {
spawnedPorts.push(port);
};
deps.waitForServerLive = async (baseUrl: string) => {
if (baseUrl.endsWith(':3999')) throw new Error('EADDRINUSE after preflight');
};
await handleBarLaunch(['--port', '3999'], deps);
expect(spawnedPorts).toEqual([3999, 3000]);
});
it('restores the prior server when replacement spawn fails', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps } = makePortDeps(ccsDir);
const spawnedPorts: number[] = [];
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.spawnDetachedServer = (port: number) => {
spawnedPorts.push(port);
if (port === 3999) throw new Error('spawn failed');
};
await handleBarLaunch(['--port', '3999'], deps);
expect(spawnedPorts).toEqual([3999, 3000]);
});
it('preserves prior discovery state when replacement and rollback both fail', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true });
const oldBarJson = JSON.stringify({
baseUrl: 'http://127.0.0.1:3000',
port: 3000,
authMode: 'loopback',
});
const oldLaunchJson = JSON.stringify({ args: ['ccs.js', 'bar', 'serve', '--port', '3000'] });
fs.writeFileSync(path.join(ccsDir, 'bar.json'), oldBarJson);
fs.writeFileSync(path.join(ccsDir, 'bar', 'launch.json'), oldLaunchJson);
const { handleBarLaunch } = await loadLaunchSubcommand();
const { deps } = makePortDeps(ccsDir);
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
deps.spawnDetachedServer = () => {
throw new Error('spawn failed');
};
await handleBarLaunch(['--port', '3999'], deps);
expect(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')).toBe(oldBarJson);
expect(fs.readFileSync(path.join(ccsDir, 'bar', 'launch.json'), 'utf8')).toBe(oldLaunchJson);
});
it('without --port, prefers the port recorded in bar.json (sticky port)', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
@@ -0,0 +1,307 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import { spawn } from 'child_process';
import {
defaultFindRunningServer,
resolveBarPort,
} from '../../../src/commands/bar/bar-server-probe';
import {
serializeBarServerProcessRecord,
getProcessBirthIdentity,
removeBarDiscoveryIfNoProcess,
removeBarServerProcessRecordIfOwned,
stopDetachedBarServer,
stopBarServerProcessFile,
stopRecordedBarServer,
} from '../../../src/commands/bar/bar-process-control';
import { parsePortFlag, validatePortArgs } from '../../../src/commands/bar/port-arg';
import { handleBarServe } from '../../../src/commands/bar/serve-subcommand';
import { handleBarStop } from '../../../src/commands/bar/stop-subcommand';
let tempHome: string;
let originalHome: string | undefined;
let originalCcsHome: string | undefined;
const liveChildren = new Set<ReturnType<typeof spawn>>();
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-lifecycle-'));
originalHome = process.env.HOME;
originalCcsHome = process.env.CCS_HOME;
process.env.HOME = tempHome;
process.env.CCS_HOME = path.join(tempHome, '.ccs');
process.exitCode = 0;
});
afterEach(() => {
for (const child of liveChildren) {
try {
child.kill('SIGKILL');
} catch {
// Already exited.
}
}
liveChildren.clear();
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
else process.env.CCS_HOME = originalCcsHome;
process.exitCode = 0;
fs.rmSync(tempHome, { recursive: true, force: true });
});
describe('strict Bar port parsing', () => {
it('rejects numeric prefixes, fractions, signs, whitespace, and out-of-range values', () => {
for (const raw of ['3999junk', '3.5', '+3999', ' 3999', '0', '65536']) {
expect(parsePortFlag(['--port', raw])).toEqual({ present: true, port: null });
}
expect(parsePortFlag(['--port', '3999'])).toEqual({ present: true, port: 3999 });
});
it('rejects unknown and duplicate launch options', () => {
expect(validatePortArgs(['--porrt', '3999'])).toBe('Unknown option: --porrt');
expect(validatePortArgs(['--port', '3999', '--port', '4000'])).toBe('Duplicate option: --port');
});
it('rejects malformed persisted ports in both discovery files', () => {
const ccsDir = process.env.CCS_HOME!;
fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true });
fs.writeFileSync(path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 3999.5 }));
fs.writeFileSync(
path.join(ccsDir, 'bar', 'launch.json'),
JSON.stringify({ args: ['ccs.js', 'bar', 'serve', '--port', '4555junk'] })
);
expect(resolveBarPort(ccsDir)).toBeNull();
});
});
describe('verified Bar process stopping', () => {
const rawRecord = serializeBarServerProcessRecord({ pid: 4321, birthIdentity: 'birth-a' });
it('does not signal when the PID birth identity changed', async () => {
let signaled = false;
const outcome = await stopRecordedBarServer(rawRecord, {
getProcessBirthIdentity: () => 'birth-b',
killProcess: () => {
signaled = true;
},
});
expect(outcome.result).toBe('identity-mismatch');
expect(signaled).toBe(false);
});
it('preserves server.pid and bar.json on mismatch, EPERM, and timeout', async () => {
for (const failure of [
'identity-mismatch',
'permission-denied',
'signal-failed',
'timeout',
] as const) {
const ccsDir = path.join(tempHome, failure);
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
const barJsonPath = path.join(ccsDir, 'bar.json');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
fs.writeFileSync(pidPath, rawRecord);
fs.writeFileSync(barJsonPath, '{}');
await handleBarStop([], {
getCcsDir: () => ccsDir,
getProcessBirthIdentity: () => (failure === 'identity-mismatch' ? 'birth-b' : 'birth-a'),
killProcess: () => {
if (failure === 'permission-denied') {
const err = new Error('not permitted') as NodeJS.ErrnoException;
err.code = 'EPERM';
throw err;
}
if (failure === 'signal-failed') throw new Error('signal transport failed');
},
waitForProcessExit: async () => (failure === 'timeout' ? 'timeout' : 'exited'),
});
expect(fs.existsSync(pidPath)).toBe(true);
expect(fs.existsSync(barJsonPath)).toBe(true);
expect(process.exitCode).toBe(1);
process.exitCode = 0;
}
});
it('never signals a legacy integer PID and gives manual recovery guidance', async () => {
const ccsDir = path.join(tempHome, 'legacy');
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
fs.writeFileSync(pidPath, '4321');
let signaled = false;
await handleBarStop([], {
getCcsDir: () => ccsDir,
killProcess: () => {
signaled = true;
},
});
expect(signaled).toBe(false);
expect(fs.existsSync(pidPath)).toBe(true);
expect(process.exitCode).toBe(1);
});
it('atomically preserves a replacement record written while the old process stops', async () => {
const pidPath = path.join(tempHome, 'race', 'server.pid');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
fs.writeFileSync(pidPath, rawRecord);
const replacement = serializeBarServerProcessRecord({
pid: 9876,
birthIdentity: 'replacement-birth',
});
const outcome = await stopBarServerProcessFile(pidPath, {
getProcessBirthIdentity: () => 'birth-a',
killProcess: () => fs.writeFileSync(pidPath, replacement),
waitForProcessExit: async () => 'exited',
});
expect(outcome.result).toBe('stopped');
expect(fs.readFileSync(pidPath, 'utf8')).toBe(replacement);
});
it('does not let an old serve cleanup unlink a replacement record', () => {
const pidPath = path.join(tempHome, 'serve-race', 'server.pid');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
const replacement = serializeBarServerProcessRecord({
pid: 9876,
birthIdentity: 'replacement-birth',
});
fs.writeFileSync(pidPath, replacement);
removeBarServerProcessRecordIfOwned(pidPath, { pid: 4321, birthIdentity: 'birth-a' });
expect(fs.readFileSync(pidPath, 'utf8')).toBe(replacement);
});
it('stops a real recorded process through the claimed process file', async () => {
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
stdio: 'ignore',
});
liveChildren.add(child);
const birthIdentity = await waitForBirthIdentity(child.pid!);
const pidPath = path.join(tempHome, 'real-stop', 'server.pid');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
fs.writeFileSync(pidPath, serializeBarServerProcessRecord({ pid: child.pid!, birthIdentity }));
const outcome = await stopBarServerProcessFile(pidPath);
expect(outcome.result).toBe('stopped');
expect(fs.existsSync(pidPath)).toBe(false);
liveChildren.delete(child);
});
it('stops a real recorded process through the launch-move stop path', async () => {
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
stdio: 'ignore',
});
liveChildren.add(child);
const birthIdentity = await waitForBirthIdentity(child.pid!);
const ccsDir = path.join(tempHome, 'real-move');
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
fs.mkdirSync(path.dirname(pidPath), { recursive: true });
fs.writeFileSync(pidPath, serializeBarServerProcessRecord({ pid: child.pid!, birthIdentity }));
await stopDetachedBarServer(ccsDir);
expect(fs.existsSync(pidPath)).toBe(false);
liveChildren.delete(child);
});
});
describe('Bar serve publication ownership', () => {
it('publishes server.pid before bar.json so old-stop cleanup preserves replacement discovery', async () => {
const ccsDir = path.join(tempHome, 'serve-publication');
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
const barJsonPath = path.join(ccsDir, 'bar.json');
const publicationOrder: string[] = [];
let oldStopRemovedDiscovery: boolean | null = null;
await handleBarServe(['--port', '4555'], {
getCcsDir: () => ccsDir,
findRunningServer: async () => null,
getPort: async () => 4555,
startServer: async () => ({ port: 4555, baseUrl: 'http://127.0.0.1:4555' }),
getProcessBirthIdentity: () => 'replacement-birth',
writeFile: (filePath, content) => {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
publicationOrder.push(filePath);
if (filePath === barJsonPath) {
oldStopRemovedDiscovery = removeBarDiscoveryIfNoProcess(barJsonPath, pidPath);
}
},
onSignal: () => {},
exit: (code) => {
throw new Error(`unexpected exit ${code}`);
},
});
expect(publicationOrder).toEqual([pidPath, barJsonPath]);
expect(oldStopRemovedDiscovery).toBe(false);
expect(fs.existsSync(pidPath)).toBe(true);
expect(JSON.parse(fs.readFileSync(barJsonPath, 'utf8')).port).toBe(4555);
});
it('conditionally rolls back its owned process record when discovery publication fails', async () => {
const ccsDir = path.join(tempHome, 'serve-rollback');
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
const barJsonPath = path.join(ccsDir, 'bar.json');
await expect(
handleBarServe(['--port', '4555'], {
getCcsDir: () => ccsDir,
findRunningServer: async () => null,
getPort: async () => 4555,
startServer: async () => ({ port: 4555, baseUrl: 'http://127.0.0.1:4555' }),
getProcessBirthIdentity: () => 'replacement-birth',
writeFile: (filePath, content) => {
if (filePath === barJsonPath) throw new Error('discovery write failed');
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
},
onSignal: () => {},
exit: (code) => {
throw new Error(`exit ${code}`);
},
})
).rejects.toThrow('exit 1');
expect(fs.existsSync(pidPath)).toBe(false);
});
});
async function waitForBirthIdentity(pid: number): Promise<string> {
for (let attempt = 0; attempt < 50; attempt += 1) {
const identity = getProcessBirthIdentity(pid);
if (identity !== null) return identity;
await new Promise<void>((resolve) => setTimeout(resolve, 20));
}
throw new Error(`Process ${pid} never became observable`);
}
describe('Bar server identity probe', () => {
it('ignores unrelated services returning 401 or 403 without a CCS proof', async () => {
for (const status of [401, 403]) {
const server = http.createServer((_req, res) => {
res.writeHead(status);
res.end();
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = (server.address() as { port: number }).port;
const ccsDir = path.join(tempHome, `status-${status}`);
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(path.join(ccsDir, 'bar.json'), JSON.stringify({ port }));
try {
const result = await defaultFindRunningServer(ccsDir);
expect(result?.port).not.toBe(port);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
});
});
@@ -15,6 +15,10 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
function processRecord(pid: number, birthIdentity = 'test-birth'): string {
return JSON.stringify({ pid, birthIdentity }, null, 2);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -24,6 +28,7 @@ let tempHome: string;
let originalCcsHome: string | undefined;
let originalConsoleLog: typeof console.log;
let originalConsoleError: typeof console.error;
let originalExitCode: number | undefined;
function captureConsole(): void {
originalConsoleLog = console.log;
@@ -113,6 +118,8 @@ beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-lifecycle-test-'));
originalCcsHome = process.env.CCS_HOME;
originalExitCode = process.exitCode;
process.exitCode = 0;
process.env.CCS_HOME = tempHome;
});
@@ -124,6 +131,7 @@ afterEach(() => {
} else {
process.env.CCS_HOME = originalCcsHome;
}
process.exitCode = originalExitCode ?? 0;
try {
fs.rmSync(tempHome, { recursive: true, force: true });
@@ -214,6 +222,7 @@ describe('serve: start new server', () => {
exit: (code: number) => {
throw new Error(`__EXIT_${code}__`);
},
getProcessBirthIdentity: () => 'test-birth',
});
// bar.json must be written
@@ -226,7 +235,10 @@ describe('serve: start new server', () => {
// server.pid must be written
const pidPath = path.join(ccsDir, 'bar', 'server.pid');
expect(writtenFiles[pidPath]).toBeDefined();
expect(writtenFiles[pidPath]).toBe(String(process.pid));
expect(JSON.parse(writtenFiles[pidPath])).toEqual({
pid: process.pid,
birthIdentity: 'test-birth',
});
// Both SIGINT and SIGTERM handlers registered
expect(signals).toContain('SIGINT');
@@ -309,7 +321,7 @@ describe('stop: SIGTERM and cleanup', () => {
const ccsDir = path.join(tempHome, '.ccs');
const barDir = path.join(ccsDir, 'bar');
fs.mkdirSync(barDir, { recursive: true });
fs.writeFileSync(path.join(barDir, 'server.pid'), '12345');
fs.writeFileSync(path.join(barDir, 'server.pid'), processRecord(12345));
fs.writeFileSync(path.join(ccsDir, 'bar.json'), '{"baseUrl":"http://127.0.0.1:3000"}');
const killed: Array<{ pid: number; signal: string }> = [];
@@ -329,6 +341,8 @@ describe('stop: SIGTERM and cleanup', () => {
killProcess: (pid: number, signal: string) => {
killed.push({ pid, signal });
},
getProcessBirthIdentity: () => 'test-birth',
waitForProcessExit: async () => 'exited',
removeFile: (filePath: string) => {
removed.push(filePath);
},
@@ -338,7 +352,7 @@ describe('stop: SIGTERM and cleanup', () => {
// Both pid and bar.json must be removed
expect(removed.some((p) => p.includes('server.pid'))).toBe(true);
expect(removed.some((p) => p.includes('bar.json'))).toBe(true);
expect(allOutput()).toMatch(/\[OK\].*SIGTERM/i);
expect(allOutput()).toMatch(/\[OK\].*stopped/i);
});
it('prints guidance and returns cleanly when no server.pid exists', async () => {
@@ -370,7 +384,8 @@ describe('stop: SIGTERM and cleanup', () => {
await handleBarStop([], {
getCcsDir: () => ccsDir,
readPidFile: () => '99999',
readPidFile: () => processRecord(99999),
getProcessBirthIdentity: () => null,
killProcess: () => {
const err = new Error('no such process') as NodeJS.ErrnoException;
err.code = 'ESRCH';
@@ -402,9 +417,9 @@ describe('stop: SIGTERM and cleanup', () => {
},
});
expect(allOutput()).toMatch(/\[X\].*invalid/i);
// Corrupted pid file must be cleaned up
expect(removed.some((p) => p.includes('server.pid'))).toBe(true);
expect(allOutput()).toMatch(/\[X\].*verified process record/i);
// Unverified state is preserved rather than risking removal of recovery evidence.
expect(removed.some((p) => p.includes('server.pid'))).toBe(false);
});
});
@@ -419,7 +434,7 @@ describe('status: running state reporting', () => {
await handleBarStatus([], {
getCcsDir: () => ccsDir,
readPidFile: () => '12345',
readPidFile: () => processRecord(12345),
isProcessAlive: () => true,
probeServer: async () => true,
readBarJsonBaseUrl: () => 'http://127.0.0.1:3000',
@@ -451,7 +466,7 @@ describe('status: running state reporting', () => {
await handleBarStatus([], {
getCcsDir: () => ccsDir,
readPidFile: () => '99999',
readPidFile: () => processRecord(99999),
isProcessAlive: () => false,
probeServer: async () => false,
readBarJsonBaseUrl: () => null,
@@ -461,13 +476,34 @@ describe('status: running state reporting', () => {
expect(allOutput()).toMatch(/99999/);
});
it('reports legacy integer PIDs as unverified and does not inspect their liveness', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarStatus } = await loadStatusSubcommand();
let inspected = false;
await handleBarStatus([], {
getCcsDir: () => ccsDir,
readPidFile: () => '12345',
isProcessAlive: () => {
inspected = true;
return true;
},
probeServer: async () => true,
readBarJsonBaseUrl: () => null,
});
expect(inspected).toBe(false);
expect(allOutput()).toMatch(/legacy.*unverified PID 12345/i);
expect(allOutput()).toMatch(/ps -p 12345 -o command=/);
});
it('reports alive-but-unreachable when PID is alive but HTTP probe fails', async () => {
const ccsDir = path.join(tempHome, '.ccs');
const { handleBarStatus } = await loadStatusSubcommand();
await handleBarStatus([], {
getCcsDir: () => ccsDir,
readPidFile: () => '12345',
readPidFile: () => processRecord(12345),
isProcessAlive: () => true,
probeServer: async () => false,
readBarJsonBaseUrl: () => 'http://127.0.0.1:3000',