import { beforeEach, describe, expect, it, vi } from 'vitest';
import { execFileSync } from 'child_process';
import { existsSync } from 'fs';
import { homedir } from 'os';
import { formatCodexCommandForDisplay, resolveCodexCliPath } from './codex-cli.js';
vi.mock('child_process', () => ({
execFileSync: vi.fn(),
}));
vi.mock('fs', () => ({
existsSync: vi.fn(),
}));
vi.mock('os', () => ({
homedir: vi.fn(),
}));
const execFileSyncMock = vi.mocked(execFileSync);
const existsSyncMock = vi.mocked(existsSync);
const homedirMock = vi.mocked(homedir);
function mockExecVersionSupport(supportedCommands: string[]): void {
execFileSyncMock.mockImplementation((command: string) => {
if (supportedCommands.includes(command)) {
return 'codex-cli 1.0.0';
}
throw new Error(`Command unavailable: ${command}`);
});
}
describe('resolveCodexCliPath', () => {
beforeEach(() => {
vi.clearAllMocks();
homedirMock.mockReturnValue('/Users/tester');
existsSyncMock.mockReturnValue(false);
});
it('prefers codex on PATH when available', () => {
mockExecVersionSupport(['codex']);
const resolved = resolveCodexCliPath({});
expect(resolved).toBe('codex');
expect(execFileSyncMock).toHaveBeenCalledWith(
'codex',
['--version'],
expect.objectContaining({ stdio: 'pipe', timeout: 5000 }),
);
});
it('falls back to system Codex app bundle path', () => {
const appPath = '/Applications/Codex.app/Contents/Resources/codex';
existsSyncMock.mockImplementation((filePath) => String(filePath) === appPath);
mockExecVersionSupport([appPath]);
const resolved = resolveCodexCliPath({});
expect(resolved).toBe(appPath);
});
it('uses CODEX_CLI_PATH override first when valid', () => {
const envPath = '/custom/codex/bin/codex';
existsSyncMock.mockImplementation((filePath) => String(filePath) === envPath);
mockExecVersionSupport([envPath]);
const resolved = resolveCodexCliPath({ CODEX_CLI_PATH: envPath });
expect(resolved).toBe(envPath);
expect(execFileSyncMock).toHaveBeenCalledWith(
envPath,
['--version'],
expect.objectContaining({ stdio: 'pipe', timeout: 5000 }),
);
});
it('returns null when no candidates execute', () => {
mockExecVersionSupport([]);
const resolved = resolveCodexCliPath({});
expect(resolved).toBeNull();
});
});
describe('formatCodexCommandForDisplay', () => {
it('quotes commands with spaces', () => {
expect(formatCodexCommandForDisplay('/Applications/Codex App/codex')).toBe('"/Applications/Codex App/codex"');
});
it('returns plain command when no spaces', () => {
expect(formatCodexCommandForDisplay('codex')).toBe('codex');
});
});