Merge pull request #850 from kaitranntt/kai/feat/issue-773-codex-runtime-target

feat: add codex runtime target and dashboard parity
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-03-29 14:43:09 -04:00
commit 6fc9aacc06
85 files changed
+7867 -208

No files matched your search

+16
View File
@@ -131,6 +131,8 @@ describe('cross-platform', () => {
assert(packageJson.bin.ccs, 'bin field should specify ccs command');
assert(packageJson.bin['ccs-droid'], 'bin field should specify ccs-droid command');
assert(packageJson.bin.ccsd, 'bin field should specify ccsd command');
assert(packageJson.bin['ccs-codex'], 'bin field should specify ccs-codex command');
assert(packageJson.bin.ccsx, 'bin field should specify ccsx command');
assert.notStrictEqual(
packageJson.bin['ccs-droid'],
packageJson.bin.ccs,
@@ -141,10 +143,24 @@ describe('cross-platform', () => {
packageJson.bin.ccsd,
'legacy ccsd alias should share the dedicated droid runtime entrypoint'
);
assert.notStrictEqual(
packageJson.bin['ccs-codex'],
packageJson.bin.ccs,
'ccs-codex should use a dedicated runtime entrypoint'
);
assert.strictEqual(
packageJson.bin['ccs-codex'],
packageJson.bin.ccsx,
'ccsx should share the dedicated codex runtime entrypoint'
);
assert(
fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-droid'])),
'dedicated droid runtime entrypoint should exist'
);
assert(
fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-codex'])),
'dedicated codex runtime entrypoint should exist'
);
assert(packageJson.scripts, 'package.json should have scripts field');
});
});
+138
View File
@@ -0,0 +1,138 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { listApiProfiles } from '../../../src/api/services/profile-reader';
import { runWithScopedConfigDir, setGlobalConfigDir } from '../../../src/utils/config-manager';
describe('profile reader target sanitization', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
let originalUnifiedMode: string | undefined;
function getScopedCcsDir(): string {
return path.join(tempHome, '.ccs');
}
async function runInScopedCcsDir<T>(fn: () => T): Promise<T> {
return await runWithScopedConfigDir(getScopedCcsDir(), fn);
}
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-reader-'));
originalCcsHome = process.env.CCS_HOME;
originalCcsDir = process.env.CCS_DIR;
originalUnifiedMode = process.env.CCS_UNIFIED_CONFIG;
process.env.CCS_HOME = tempHome;
delete process.env.CCS_DIR;
delete process.env.CCS_UNIFIED_CONFIG;
setGlobalConfigDir(undefined);
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (originalCcsDir === undefined) {
delete process.env.CCS_DIR;
} else {
process.env.CCS_DIR = originalCcsDir;
}
if (originalUnifiedMode === undefined) {
delete process.env.CCS_UNIFIED_CONFIG;
} else {
process.env.CCS_UNIFIED_CONFIG = originalUnifiedMode;
}
setGlobalConfigDir(undefined);
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('normalizes legacy stored codex targets back to claude for profiles and variants', async () => {
const ccsDir = getScopedCcsDir();
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.json'),
JSON.stringify(
{
profiles: { demo: '~/.ccs/demo.settings.json' },
profile_targets: { demo: 'codex' },
cliproxy: {
routed: {
provider: 'codex',
settings: '~/.ccs/routed.settings.json',
target: 'codex',
},
},
},
null,
2
) + '\n'
);
fs.writeFileSync(
path.join(ccsDir, 'demo.settings.json'),
JSON.stringify(
{ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } },
null,
2
) + '\n'
);
const result = await runInScopedCcsDir(() => listApiProfiles());
expect(result.profiles).toHaveLength(1);
expect(result.profiles[0]?.target).toBe('claude');
expect(result.variants).toHaveLength(1);
expect(result.variants[0]?.target).toBe('claude');
});
it('normalizes unified stored codex targets back to claude for profiles and variants', async () => {
const ccsDir = getScopedCcsDir();
fs.mkdirSync(ccsDir, { recursive: true });
process.env.CCS_UNIFIED_CONFIG = '1';
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 12',
'profiles:',
' demo:',
' type: api',
' settings: ~/.ccs/demo.settings.json',
' target: codex',
'cliproxy:',
' oauth_accounts: {}',
' providers: []',
' variants:',
' routed:',
' provider: codex',
' settings: ~/.ccs/routed.settings.json',
' target: codex',
'',
].join('\n'),
'utf8'
);
fs.writeFileSync(
path.join(ccsDir, 'demo.settings.json'),
JSON.stringify(
{ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } },
null,
2
) + '\n'
);
const result = await runInScopedCcsDir(() => listApiProfiles());
expect(result.profiles).toHaveLength(1);
expect(result.profiles[0]?.target).toBe('claude');
expect(result.variants).toHaveLength(1);
expect(result.variants[0]?.target).toBe('claude');
});
});
@@ -75,6 +75,13 @@ describe('api-command arg parser', () => {
]);
});
test('rejects runtime-only codex as a persisted API target value', () => {
const parsed = parseApiCommandArgs(['my-api', '--target', 'codex']);
expect(parsed.target).toBeUndefined();
expect(parsed.errors).toEqual(['Invalid --target value "codex". Use: claude or droid']);
});
test('collects missing-value error for --target with no value', () => {
const parsed = parseApiCommandArgs(['my-api', '--target']);
@@ -26,6 +26,13 @@ describe('cliproxy variant arg parser', () => {
expect(parsed.errors).toEqual(['Missing value for --target']);
});
test('rejects runtime-only codex as a persisted variant target value', () => {
const parsed = parseProfileArgs(['variant-a', '--target', 'codex']);
expect(parsed.target).toBeUndefined();
expect(parsed.errors).toEqual(['Invalid --target value "codex". Use: claude or droid']);
});
test('uses last --target value when repeated', () => {
const parsed = parseProfileArgs(['variant-a', '--target', 'claude', '--target=droid']);
@@ -99,6 +99,18 @@ describe('help command parity', () => {
expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(true);
});
test('root help documents native Codex runtime alias and runtime-only scope', async () => {
const lines: string[] = [];
await handleHelpCommand((line) => lines.push(line));
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs-codex <profile> [args]')).toBe(true);
expect(rendered.includes('ccsx <profile> [args]')).toBe(true);
expect(rendered.includes('ccs --target codex')).toBe(true);
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
expect(rendered.includes('codex (runtime-only)')).toBe(true);
});
test('api help documents create-time Claude [1m] flags and entitlement warning', async () => {
const lines: string[] = [];
await showApiCommandHelp((line) => lines.push(line));
@@ -113,4 +125,14 @@ describe('help command parity', () => {
true
);
});
test('api help documents Codex bridge runtime launch separately from persisted targets', async () => {
const lines: string[] = [];
await showApiCommandHelp((line) => lines.push(line));
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('ccs api create codex-api --cliproxy-provider codex')).toBe(true);
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
expect(rendered.includes('Default target: claude or droid (create)')).toBe(true);
});
});
@@ -71,13 +71,35 @@ describe('ccsd alias integration', () => {
expect(path.basename(argvPath)).toBe('ccs-droid');
});
it('should preserve ccs-codex symlink basename in argv[1] under node', () => {
if (process.platform === 'win32') {
return;
}
const argvPath = probeArgvPath('ccs-codex');
expect(path.basename(argvPath)).toBe('ccs-codex');
});
it('should preserve ccsx symlink basename in argv[1] under node', () => {
if (process.platform === 'win32') {
return;
}
const argvPath = probeArgvPath('ccsx');
expect(path.basename(argvPath)).toBe('ccsx');
});
it('should preserve extension-style alias basenames for wrapper compatibility', () => {
const cmdArgvPath = probeArgvPathDirect('ccsd.cmd');
const ps1ArgvPath = probeArgvPathDirect('ccsd.ps1');
const explicitCmdArgvPath = probeArgvPathDirect('ccs-droid.cmd');
const codexCmdArgvPath = probeArgvPathDirect('ccs-codex.cmd');
const codexShortCmdArgvPath = probeArgvPathDirect('ccsx.cmd');
expect(path.basename(cmdArgvPath)).toBe('ccsd.cmd');
expect(path.basename(ps1ArgvPath)).toBe('ccsd.ps1');
expect(path.basename(explicitCmdArgvPath)).toBe('ccs-droid.cmd');
expect(path.basename(codexCmdArgvPath)).toBe('ccs-codex.cmd');
expect(path.basename(codexShortCmdArgvPath)).toBe('ccsx.cmd');
});
});
+188
View File
@@ -0,0 +1,188 @@
import { describe, expect, test } from 'bun:test';
import { CodexAdapter } from '../../../src/targets/codex-adapter';
describe('CodexAdapter', () => {
const adapter = new CodexAdapter();
test('supports only adapter-level default and cliproxy profile types', () => {
expect(adapter.supportsProfileType('default')).toBe(true);
expect(adapter.supportsProfileType('cliproxy')).toBe(true);
expect(adapter.supportsProfileType('settings')).toBe(false);
expect(adapter.supportsProfileType('account')).toBe(false);
expect(adapter.supportsProfileType('copilot')).toBe(false);
});
test('passes default-mode args through unchanged', () => {
expect(
adapter.buildArgs('default', ['--search'], {
profileType: 'default',
})
).toEqual(['--search']);
});
test('translates default-mode reasoning overrides into transient codex config', () => {
const args = adapter.buildArgs('default', ['--search'], {
profileType: 'default',
creds: {
profile: 'default',
baseUrl: '',
apiKey: '',
reasoningOverride: 'medium',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
features: ['config-overrides'],
},
});
expect(args).toEqual(['-c', 'model_reasoning_effort="medium"', '--search']);
});
test('rejects default-mode reasoning overrides when codex lacks config override support', () => {
expect(() =>
adapter.buildArgs('default', ['--search'], {
profileType: 'default',
creds: {
profile: 'default',
baseUrl: '',
apiKey: '',
reasoningOverride: 'high',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
version: 'codex-cli 0.1.0',
features: [],
},
})
).toThrow(/does not advertise --config overrides/);
});
test('injects transient config overrides for CCS-backed launches', () => {
const args = adapter.buildArgs('codex', ['--search'], {
profileType: 'cliproxy',
creds: {
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
model: 'gpt-5.4',
reasoningOverride: 'high',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
features: ['config-overrides'],
},
});
expect(args).toContain('-c');
expect(args).toContain('model_provider="ccs_runtime"');
expect(args).toContain('model_providers.ccs_runtime.env_key="CCS_CODEX_API_KEY"');
expect(args).toContain('model="gpt-5.4"');
expect(args).toContain('model_reasoning_effort="high"');
expect(args.at(-1)).toBe('--search');
});
test('fails fast when Codex binary lacks config override support', () => {
expect(() =>
adapter.buildArgs('codex', [], {
profileType: 'cliproxy',
creds: {
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
version: 'codex-cli 0.1.0',
features: [],
},
})
).toThrow(/does not advertise --config overrides/);
});
test('rejects native Codex provider-selection flags for CCS-backed launches', () => {
expect(() =>
adapter.buildArgs('codex', ['--profile', 'other', '--search'], {
profileType: 'cliproxy',
creds: {
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
features: ['config-overrides'],
},
})
).toThrow(/does not allow --profile\/-p/);
});
test('rejects user-supplied --config overrides for CCS-backed launches', () => {
const options = {
profileType: 'cliproxy' as const,
creds: {
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
features: ['config-overrides'],
},
};
expect(() => adapter.buildArgs('codex', ['-c', 'model="other"', '--search'], options)).toThrow(
/does not allow --config\/-c/
);
expect(() =>
adapter.buildArgs('codex', ['--config=model="other"', '--search'], options)
).toThrow(/does not allow --config\/-c/);
});
test('rejects unsupported reasoning override values for CCS-backed launches', () => {
expect(() =>
adapter.buildArgs('codex', ['--search'], {
profileType: 'cliproxy',
creds: {
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
reasoningOverride: 8192,
},
binaryInfo: {
path: '/tmp/codex',
needsShell: false,
features: ['config-overrides'],
},
})
).toThrow(/supports reasoning levels only/);
});
test('injects CCS_CODEX_API_KEY for CCS-backed launches only', () => {
const settingsEnv = adapter.buildEnv(
{
profile: 'codex',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
apiKey: 'cliproxy-token',
},
'cliproxy'
);
expect(settingsEnv.CCS_CODEX_API_KEY).toBe('cliproxy-token');
const defaultEnv = adapter.buildEnv(
{
profile: 'default',
baseUrl: '',
apiKey: '',
},
'default'
);
expect(defaultEnv.CCS_CODEX_API_KEY).toBeUndefined();
});
});
+75
View File
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { detectCodexCli, getCodexBinaryInfo } from '../../../src/targets/codex-detector';
describe('codex-detector', () => {
let tmpDir: string;
let originalPath: string | undefined;
let originalCodexPath: string | undefined;
const originalPlatform = process.platform;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-detector-test-'));
originalPath = process.env.PATH;
originalCodexPath = process.env.CCS_CODEX_PATH;
process.env.PATH = '';
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
if (originalPath !== undefined) process.env.PATH = originalPath;
else delete process.env.PATH;
if (originalCodexPath !== undefined) process.env.CCS_CODEX_PATH = originalCodexPath;
else delete process.env.CCS_CODEX_PATH;
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('should prefer CCS_CODEX_PATH when it points to a file', () => {
const fakeCodex = path.join(tmpDir, 'codex');
fs.writeFileSync(fakeCodex, '#!/bin/sh\necho codex\n');
process.env.CCS_CODEX_PATH = fakeCodex;
expect(detectCodexCli()).toBe(fakeCodex);
});
it('should return null when CCS_CODEX_PATH points to a directory', () => {
process.env.CCS_CODEX_PATH = tmpDir;
expect(detectCodexCli()).toBeNull();
});
it('should return binary info without throwing when help probing fails', () => {
const fakeCodex = path.join(tmpDir, 'codex');
fs.writeFileSync(fakeCodex, '');
process.env.CCS_CODEX_PATH = fakeCodex;
expect(() => getCodexBinaryInfo()).not.toThrow();
});
it('probes Windows cmd wrappers through the shell so config override support is detected', () => {
const fakeCodex = path.join(tmpDir, 'codex.cmd');
fs.writeFileSync(fakeCodex, '');
process.env.CCS_CODEX_PATH = fakeCodex;
Object.defineProperty(process, 'platform', { value: 'win32' });
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation((command, args) => {
return String(command).includes('cmd.exe') && Array.isArray(args) && args.join(' ').includes('--help')
? 'Codex CLI\n -c, --config <key=value>\n'
: 'codex-cli 0.118.0-alpha.3';
});
const info = getCodexBinaryInfo();
expect(execFileSyncSpy).toHaveBeenCalled();
expect(info?.needsShell).toBe(true);
expect(info?.features).toContain('config-overrides');
execFileSyncSpy.mockRestore();
});
});
@@ -0,0 +1,197 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
interface RunResult {
status: number | null;
stdout: string;
stderr: string;
}
function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
const ccsEntry = path.join(process.cwd(), 'src', 'ccs.ts');
const result = spawnSync(process.execPath, [ccsEntry, ...args], {
encoding: 'utf8',
env,
timeout: 20000,
});
return {
status: result.status,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
function readLoggedCodexCalls(logPath: string): string[][] {
if (!fs.existsSync(logPath)) {
return [];
}
return fs
.readFileSync(logPath, 'utf8')
.trim()
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line) as string[]);
}
describe('codex runtime integration', () => {
let tmpHome: string;
let ccsDir: string;
let fakeCodexPath: string;
let codexArgsLogPath: string;
let emptyPathDir: string;
beforeEach(() => {
if (process.platform === 'win32') {
return;
}
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-route-it-'));
ccsDir = path.join(tmpHome, '.ccs');
fakeCodexPath = path.join(tmpHome, 'fake-codex.js');
codexArgsLogPath = path.join(tmpHome, 'codex-args.log');
emptyPathDir = path.join(tmpHome, 'empty-bin');
fs.mkdirSync(ccsDir, { recursive: true });
fs.mkdirSync(emptyPathDir, { recursive: true });
fs.writeFileSync(
fakeCodexPath,
`#!/usr/bin/env node
const fs = require('fs');
const out = process.env.CCS_TEST_CODEX_ARGS_OUT;
if (out) {
fs.appendFileSync(out, JSON.stringify(process.argv.slice(2)) + '\\n');
}
if (process.argv[2] === '--version') {
process.stdout.write(process.env.CCS_TEST_CODEX_VERSION || 'codex-cli 0.118.0-alpha.3');
process.exit(0);
}
if (process.argv[2] === '--help') {
process.stdout.write(
process.env.CCS_TEST_CODEX_HELP ||
' -c, --config <key=value>\\n -p, --profile <CONFIG_PROFILE>\\n'
);
process.exit(0);
}
process.exit(0);
`,
{ encoding: 'utf8', mode: 0o755 }
);
fs.chmodSync(fakeCodexPath, 0o755);
});
afterEach(() => {
if (process.platform === 'win32') {
return;
}
fs.rmSync(tmpHome, { recursive: true, force: true });
});
it('ignores numeric CCS_THINKING env overrides for native Codex default mode', () => {
if (process.platform === 'win32') return;
const result = runCcs(['default', '--target', 'codex', 'fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_THINKING: '8192',
});
expect(result.status).toBe(0);
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls.at(-1)).toEqual(['fix failing tests']);
});
it('ignores off-style CCS_THINKING env overrides for native Codex default mode', () => {
if (process.platform === 'win32') return;
const result = runCcs(['default', '--target', 'codex', 'fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_THINKING: 'off',
});
expect(result.status).toBe(0);
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls.at(-1)).toEqual(['fix failing tests']);
});
it('fails fast when native Codex reasoning overrides need unsupported --config support', () => {
if (process.platform === 'win32') return;
const result = runCcs(['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
CCS_TEST_CODEX_HELP: ' -p, --profile <CONFIG_PROFILE>\\n',
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('does not advertise --config overrides');
const calls = readLoggedCodexCalls(codexArgsLogPath);
expect(calls).toEqual([['--version'], ['--help']]);
});
it('reports unsupported generic settings profiles before Codex install guidance', () => {
if (process.platform === 'win32') return;
const settingsPath = path.join(ccsDir, 'myglm.settings.json');
const configPath = path.join(ccsDir, 'config.json');
fs.writeFileSync(
settingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'https://example.invalid/anthropic',
ANTHROPIC_AUTH_TOKEN: 'test-token',
ANTHROPIC_MODEL: 'gpt-5.4',
},
},
null,
2
)
);
fs.writeFileSync(
configPath,
JSON.stringify(
{
profiles: {
myglm: settingsPath,
},
},
null,
2
)
);
const result = runCcs(['myglm', '--target', 'codex', 'fix failing tests'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
PATH: emptyPathDir,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain(
'Codex CLI currently supports native default sessions and Codex-routed CLIProxy sessions only.'
);
expect(result.stderr).not.toContain('Install a recent @openai/codex build');
});
});
@@ -0,0 +1,135 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
interface RunResult {
status: number | null;
stdout: string;
stderr: string;
}
function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
const ccsEntry = path.join(process.cwd(), 'src', 'ccs.ts');
const result = spawnSync(process.execPath, [ccsEntry, ...args], {
encoding: 'utf8',
env,
timeout: 20000,
});
return {
status: result.status,
stdout: result.stdout || '',
stderr: result.stderr || '',
};
}
describe('Codex settings bridge launch', () => {
let tmpHome = '';
let ccsDir = '';
let settingsPath = '';
let fakeCodexPath = '';
let codexArgsLogPath = '';
let codexEnvLogPath = '';
let baseEnv: NodeJS.ProcessEnv;
beforeEach(() => {
if (process.platform === 'win32') {
return;
}
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-bridge-launch-'));
ccsDir = path.join(tmpHome, '.ccs');
settingsPath = path.join(ccsDir, 'codex-api.settings.json');
fakeCodexPath = path.join(tmpHome, 'fake-codex.sh');
codexArgsLogPath = path.join(tmpHome, 'codex-args.txt');
codexEnvLogPath = path.join(tmpHome, 'codex-env.txt');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.json'),
JSON.stringify({ profiles: { 'codex-api': settingsPath } }, null, 2) + '\n'
);
fs.writeFileSync(
settingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
ANTHROPIC_AUTH_TOKEN: 'bridge-token',
ANTHROPIC_MODEL: 'gpt-5.3-codex',
},
},
null,
2
) + '\n'
);
fs.writeFileSync(
fakeCodexPath,
`#!/bin/sh
if [ "$1" = "--version" ]; then
echo "codex-cli 0.118.0-alpha.3"
exit 0
fi
if [ "$1" = "--help" ]; then
cat <<'EOF'
Codex CLI
-c, --config <key=value>
EOF
exit 0
fi
printf "%s\\n" "$@" > "${codexArgsLogPath}"
printf "%s" "$CCS_CODEX_API_KEY" > "${codexEnvLogPath}"
exit 0
`,
{ encoding: 'utf8', mode: 0o755 }
);
fs.chmodSync(fakeCodexPath, 0o755);
baseEnv = {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
CCS_DEBUG: '1',
};
});
afterEach(() => {
if (process.platform === 'win32') {
return;
}
fs.rmSync(tmpHome, { recursive: true, force: true });
});
it('launches Codex bridge settings profiles and injects runtime overrides', () => {
if (process.platform === 'win32') return;
const result = runCcs(['codex-api', '--target', 'codex', '--effort', 'high', 'smoke'], baseEnv);
expect(result.status).toBe(0);
expect(result.stderr).not.toContain('does not support this profile');
const argsLog = fs.readFileSync(codexArgsLogPath, 'utf8');
expect(argsLog).toContain('model_provider="ccs_runtime"');
expect(argsLog).toContain('model_providers.ccs_runtime.base_url="http://127.0.0.1:8317/api/provider/codex"');
expect(argsLog).toContain('model_reasoning_effort="high"');
expect(argsLog).toContain('smoke');
expect(fs.readFileSync(codexEnvLogPath, 'utf8')).toBe('bridge-token');
});
it('rejects native Codex profile flags when CCS manages the bridge runtime', () => {
if (process.platform === 'win32') return;
const result = runCcs(['codex-api', '--target', 'codex', '--profile', 'other', 'smoke'], baseEnv);
expect(result.status).toBe(1);
expect(result.stderr).toContain('does not allow --profile/-p');
expect(fs.existsSync(codexArgsLogPath)).toBe(false);
});
});
@@ -13,6 +13,7 @@ import {
getRegisteredTargets,
ClaudeAdapter,
DroidAdapter,
CodexAdapter,
} from '../../../src/targets';
describe('target-registry', () => {
@@ -20,6 +21,7 @@ describe('target-registry', () => {
// Re-register adapters (registry is module-scoped singleton)
registerTarget(new ClaudeAdapter());
registerTarget(new DroidAdapter());
registerTarget(new CodexAdapter());
});
it('should register and retrieve claude adapter', () => {
@@ -39,6 +41,12 @@ describe('target-registry', () => {
expect(adapter.type).toBe('claude');
});
it('should register and retrieve codex adapter', () => {
const adapter = getTarget('codex');
expect(adapter.type).toBe('codex');
expect(adapter.displayName).toBe('Codex CLI');
});
it('should throw for unknown target', () => {
expect(() => getTarget('unknown' as never)).toThrow(/Unknown target "unknown"/);
});
@@ -46,6 +54,7 @@ describe('target-registry', () => {
it('should check target existence', () => {
expect(hasTarget('claude')).toBe(true);
expect(hasTarget('droid')).toBe(true);
expect(hasTarget('codex')).toBe(true);
expect(hasTarget('unknown' as never)).toBe(false);
});
@@ -53,6 +62,7 @@ describe('target-registry', () => {
const targets = getRegisteredTargets();
expect(targets).toContain('claude');
expect(targets).toContain('droid');
expect(targets).toContain('codex');
});
});
+88 -3
View File
@@ -7,6 +7,7 @@ import { resolveTargetType, stripTargetFlag } from '../../../src/targets/target-
describe('resolveTargetType', () => {
const originalArgv = process.argv;
const originalDroidAliases = process.env.CCS_DROID_ALIASES;
const originalCodexAliases = process.env.CCS_CODEX_ALIASES;
const originalTargetAliases = process.env.CCS_TARGET_ALIASES;
const originalInternalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET;
@@ -18,6 +19,12 @@ describe('resolveTargetType', () => {
process.env.CCS_DROID_ALIASES = originalDroidAliases;
}
if (originalCodexAliases === undefined) {
delete process.env.CCS_CODEX_ALIASES;
} else {
process.env.CCS_CODEX_ALIASES = originalCodexAliases;
}
if (originalTargetAliases === undefined) {
delete process.env.CCS_TARGET_ALIASES;
} else {
@@ -41,6 +48,11 @@ describe('resolveTargetType', () => {
expect(resolveTargetType(['--target', 'droid'])).toBe('droid');
});
it('should detect --target codex', () => {
process.argv = ['node', 'ccs'];
expect(resolveTargetType(['--target', 'codex'])).toBe('codex');
});
it('should detect --target claude', () => {
process.argv = ['node', 'ccs'];
expect(resolveTargetType(['--target', 'claude'])).toBe('claude');
@@ -56,6 +68,11 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([], { target: 'invalid-target' as never })).toBe('claude');
});
it('should ignore runtime-only codex target when it appears in persisted profile config', () => {
process.argv = ['node', 'ccs'];
expect(resolveTargetType([], { target: 'codex' })).toBe('claude');
});
it('should prioritize --target flag over profile config', () => {
process.argv = ['node', 'ccs'];
expect(resolveTargetType(['--target', 'claude'], { target: 'droid' })).toBe('claude');
@@ -71,15 +88,31 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should detect built-in ccs-codex argv[0] alias', () => {
process.argv = ['node', 'ccs-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should detect built-in ccsx argv[0] alias', () => {
process.argv = ['node', 'ccsx'];
expect(resolveTargetType([])).toBe('codex');
});
it('should detect custom target aliases from CCS_TARGET_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'droid=droidx,my-droid';
process.argv = ['node', 'my-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should detect codex aliases from CCS_TARGET_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'codex=codexx,team-codex';
process.argv = ['node', 'team-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should ignore unsupported targets in CCS_TARGET_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'codex=ccsx;droid=ccs-droid-custom';
process.argv = ['node', 'ccsx'];
process.env.CCS_TARGET_ALIASES = 'not-a-target=mystery-codex;droid=ccs-droid-custom';
process.argv = ['node', 'mystery-codex'];
expect(resolveTargetType([])).toBe('claude');
});
@@ -89,6 +122,12 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should detect custom argv[0] aliases from CCS_CODEX_ALIASES', () => {
process.env.CCS_CODEX_ALIASES = 'codexx,my-codex';
process.argv = ['node', 'my-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should merge CCS_TARGET_ALIASES and CCS_DROID_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'droid=team-droid';
process.env.CCS_DROID_ALIASES = 'legacy-droid';
@@ -100,6 +139,17 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should merge CCS_TARGET_ALIASES and CCS_CODEX_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'codex=team-codex';
process.env.CCS_CODEX_ALIASES = 'legacy-codex';
process.argv = ['node', 'team-codex'];
expect(resolveTargetType([])).toBe('codex');
process.argv = ['node', 'legacy-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should ignore invalid custom alias entries', () => {
process.env.CCS_DROID_ALIASES = 'valid_alias,../bad,';
process.argv = ['node', '../bad'];
@@ -112,6 +162,12 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should detect internal entry target for codex runtime bins', () => {
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
process.argv = ['node', 'ccs'];
expect(resolveTargetType([])).toBe('codex');
});
it('should normalize argv[0] and custom aliases case-insensitively', () => {
process.env.CCS_DROID_ALIASES = 'DroidCaps';
process.argv = ['node', 'DROIDCAPS'];
@@ -128,11 +184,21 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should strip .cmd extension on built-in codex alias', () => {
process.argv = ['node', 'ccs-codex.cmd'];
expect(resolveTargetType([])).toBe('codex');
});
it('should strip .bat extension on Windows argv[0]', () => {
process.argv = ['node', 'ccsd.bat'];
expect(resolveTargetType([])).toBe('droid');
});
it('should strip .bat extension on codex shortcut alias', () => {
process.argv = ['node', 'ccsx.bat'];
expect(resolveTargetType([])).toBe('codex');
});
it('should strip .ps1 extension on Windows argv[0]', () => {
process.argv = ['node', 'ccsd.ps1'];
expect(resolveTargetType([])).toBe('droid');
@@ -153,6 +219,11 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should handle full path argv[0] for ccs-codex', () => {
process.argv = ['node', '/usr/local/bin/ccs-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should prioritize --target over argv[0]', () => {
process.argv = ['node', 'ccsd'];
expect(resolveTargetType(['--target', 'claude'])).toBe('claude');
@@ -176,8 +247,10 @@ describe('resolveTargetType', () => {
});
it('should keep reserved command names authoritative', () => {
process.env.CCS_TARGET_ALIASES = 'claude=ccs,ccs-droid,ccsd;droid=mydroid';
process.env.CCS_TARGET_ALIASES =
'claude=ccs,ccs-droid,ccsd,ccs-codex,ccsx;droid=mydroid;codex=mycodex';
process.env.CCS_DROID_ALIASES = 'ccs,ccs-droid,ccsd,legacy-droid';
process.env.CCS_CODEX_ALIASES = 'ccs,ccs-codex,ccsx,legacy-codex';
process.argv = ['node', 'ccs'];
expect(resolveTargetType([])).toBe('claude');
@@ -188,11 +261,23 @@ describe('resolveTargetType', () => {
process.argv = ['node', 'ccsd'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'ccs-codex'];
expect(resolveTargetType([])).toBe('codex');
process.argv = ['node', 'ccsx'];
expect(resolveTargetType([])).toBe('codex');
process.argv = ['node', 'mydroid'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'legacy-droid'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'mycodex'];
expect(resolveTargetType([])).toBe('codex');
process.argv = ['node', 'legacy-codex'];
expect(resolveTargetType([])).toBe('codex');
});
it('should throw for invalid --target value', () => {
@@ -0,0 +1,86 @@
import { describe, expect, test } from 'bun:test';
import { evaluateTargetRuntimeCompatibility } from '../../../src/targets/target-runtime-compatibility';
describe('evaluateTargetRuntimeCompatibility', () => {
test('supports native Codex default sessions', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'default',
}).supported
).toBe(true);
});
test('supports Codex CLIProxy provider sessions only for provider codex', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'cliproxy',
cliproxyProvider: 'codex',
isComposite: false,
}).supported
).toBe(true);
const unsupported = evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'cliproxy',
cliproxyProvider: 'gemini',
isComposite: false,
});
expect(unsupported.supported).toBe(false);
expect(unsupported.reason).toMatch(/only supports CLIProxy provider "codex"/);
});
test('rejects composite CLIProxy variants on Codex target', () => {
const compatibility = evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'cliproxy',
cliproxyProvider: 'codex',
isComposite: true,
});
expect(compatibility.supported).toBe(false);
expect(compatibility.reason).toMatch(/does not support composite CLIProxy variants/);
});
test('supports only Codex bridge API profiles on Codex target', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'settings',
cliproxyBridgeProvider: 'codex',
}).supported
).toBe(true);
const compatibility = evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'settings',
cliproxyBridgeProvider: 'gemini',
});
expect(compatibility.supported).toBe(false);
expect(compatibility.reason).toMatch(/only supports CLIProxy Codex bridge profiles/);
const genericSettingsCompatibility = evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'settings',
});
expect(genericSettingsCompatibility.supported).toBe(false);
expect(genericSettingsCompatibility.reason).toMatch(/currently supports native default sessions/);
});
test('rejects account and copilot profiles on Codex target', () => {
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'account',
}).supported
).toBe(false);
expect(
evaluateTargetRuntimeCompatibility({
target: 'codex',
profileType: 'copilot',
}).supported
).toBe(false);
});
});
@@ -0,0 +1,602 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
CodexRawConfigConflictError,
CodexRawConfigValidationError,
getCodexDashboardDiagnostics,
getCodexRawConfig,
patchCodexConfig,
resolveCodexConfigPaths,
saveCodexRawConfig,
summarizeCodexFeatureFlags,
summarizeCodexMcpServers,
summarizeCodexModelProviders,
summarizeCodexProjectTrust,
} from '../../../src/web-server/services/codex-dashboard-service';
const testRoot = path.join(os.tmpdir(), `ccs-codex-dashboard-test-${Date.now()}`);
const codexHome = path.join(testRoot, '.codex-home');
const codexStubPath = path.join(testRoot, 'codex');
function writeCodexStub(options?: { helpText?: string; version?: string }) {
const helpText =
options?.helpText ?? ' -c, --config <key=value>\n -p, --profile <CONFIG_PROFILE>\n';
const version = options?.version ?? 'codex-cli 0.118.0-alpha.3';
fs.writeFileSync(
codexStubPath,
`#!/bin/sh
if [ "$1" = "--version" ]; then
printf '%s\\n' "${version}"
exit 0
fi
if [ "$1" = "--help" ]; then
printf '%s' "${helpText}"
exit 0
fi
exit 0
`
);
fs.chmodSync(codexStubPath, 0o755);
}
beforeEach(() => {
fs.mkdirSync(testRoot, { recursive: true });
fs.mkdirSync(codexHome, { recursive: true });
writeCodexStub();
process.env.CODEX_HOME = codexHome;
process.env.CCS_CODEX_PATH = codexStubPath;
});
afterEach(() => {
delete process.env.CODEX_HOME;
delete process.env.CCS_CODEX_PATH;
if (fs.existsSync(testRoot)) {
fs.rmSync(testRoot, { recursive: true, force: true });
}
});
describe('codex-dashboard-service', () => {
it('resolves codex config paths with CODEX_HOME override', () => {
const resolved = resolveCodexConfigPaths({
env: {
CODEX_HOME: './custom-codex-home',
} as NodeJS.ProcessEnv,
homeDir: '/Users/tester',
});
expect(resolved.baseDir).toBe(path.resolve('./custom-codex-home'));
expect(resolved.baseDirDisplay).toBe('$CODEX_HOME');
expect(resolved.configPath).toBe(path.join(path.resolve('./custom-codex-home'), 'config.toml'));
expect(resolved.configDisplayPath).toBe('$CODEX_HOME/config.toml');
});
it('summarizes model providers with auth and header metadata', () => {
const summary = summarizeCodexModelProviders({
cliproxy: {
base_url: 'http://127.0.0.1:8317/api/provider/codex',
env_key: 'CLIPROXY_API_KEY',
wire_api: 'responses',
http_headers: { 'x-test': '1' },
},
local: {
base_url: 'http://localhost:11434/v1',
experimental_bearer_token: 'secret',
supports_websockets: true,
},
});
expect(summary.length).toBe(2);
expect(summary[0].name).toBe('cliproxy');
expect(summary[0].envKey).toBe('CLIPROXY_API_KEY');
expect(summary[0].hasHttpHeaders).toBe(true);
expect(summary[1].usesExperimentalBearerToken).toBe(true);
});
it('summarizes feature flags, project trust, and mcp servers', () => {
const features = summarizeCodexFeatureFlags({
multi_agent: true,
shell_snapshot: false,
custom_mode: 'beta',
});
const projects = summarizeCodexProjectTrust({
'/tmp/a': { trust_level: 'trusted' },
'/tmp/b': { trust_level: 'untrusted' },
});
const servers = summarizeCodexMcpServers({
stdio: {
command: 'npx',
enabled_tools: ['browser_snapshot'],
},
remote: {
url: 'https://example.test/mcp',
bearer_token: 'not-allowed-inline',
required: true,
},
});
expect(features.enabled.map((feature) => feature.name)).toEqual(['multi_agent']);
expect(features.disabled.map((feature) => feature.name)).toEqual(['shell_snapshot']);
expect(features.all.find((feature) => feature.name === 'custom_mode')?.state).toBe('custom');
expect(projects.length).toBe(2);
expect(projects[0].trustLevel).toBe('trusted');
expect(servers[0].transport).toBe('streamable-http');
expect(servers[0].usesInlineBearerToken).toBe(true);
});
it('returns raw config payload for missing config.toml', async () => {
const raw = await getCodexRawConfig();
expect(raw.exists).toBe(false);
expect(raw.path).toBe('$CODEX_HOME/config.toml');
expect(raw.rawText).toBe('');
expect(raw.config).toBeNull();
expect(raw.readError).toBeNull();
});
it('returns parseError when config.toml is invalid TOML', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n[features\n');
const raw = await getCodexRawConfig();
expect(raw.exists).toBe(true);
expect(raw.parseError).toBeString();
expect(raw.config).toBeNull();
});
it('returns readError when config.toml is a symlink', async () => {
const configPath = path.join(codexHome, 'config.toml');
const targetPath = path.join(testRoot, 'linked.toml');
fs.writeFileSync(targetPath, 'model = "gpt-5.4"\n');
fs.symlinkSync(targetPath, configPath);
const raw = await getCodexRawConfig();
expect(raw.exists).toBe(true);
expect(raw.readError).toContain('Refusing symlink file');
expect(raw.config).toBeNull();
});
it('includes docs links, support matrix, and config summaries in diagnostics', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
`model = "gpt-5.4"
profile = "work"
model_provider = "cliproxy"
approval_policy = "never"
sandbox_mode = "danger-full-access"
web_search = "live"
[features]
multi_agent = true
shell_snapshot = false
runtime_metrics = true
[model_providers.cliproxy]
name = "CLIProxyAPI"
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"
[projects."/tmp/project-a"]
trust_level = "trusted"
[projects."/tmp/project-b"]
trust_level = "untrusted"
[mcp_servers.playwright]
command = "npx"
args = ["@playwright/mcp@latest"]
enabled_tools = ["browser_snapshot"]
tool_timeout_sec = 30
[profiles.work]
model = "gpt-5.4"
`
);
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.binary.installed).toBe(true);
expect(diagnostics.binary.supportsConfigOverrides).toBe(true);
expect(diagnostics.config.model).toBe('gpt-5.4');
expect(diagnostics.config.activeProfile).toBe('work');
expect(diagnostics.config.modelProvider).toBe('cliproxy');
expect(diagnostics.config.profileCount).toBe(1);
expect(diagnostics.config.modelProviderCount).toBe(1);
expect(diagnostics.config.featureCount).toBe(3);
expect(diagnostics.config.enabledFeatures.map((feature) => feature.name)).toEqual([
'multi_agent',
'runtime_metrics',
]);
expect(diagnostics.config.trustedProjectCount).toBe(1);
expect(diagnostics.config.untrustedProjectCount).toBe(1);
expect(diagnostics.config.mcpServerCount).toBe(1);
expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
expect(diagnostics.supportMatrix.some((entry) => entry.id === 'default')).toBe(true);
});
it('summarizes granular approval policies without flattening them to null', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
'approval_policy = { granular = { edit = "on-request" } }\n'
);
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.config.approvalPolicy).toBe('granular (custom)');
});
it('warns when active profile is missing, config overrides are unavailable, or risky fields exist', async () => {
writeCodexStub({ helpText: ' -p, --profile <CONFIG_PROFILE>\n' });
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
`profile = "missing-profile"
[model_providers.local]
experimental_bearer_token = "secret"
[mcp_servers.remote]
url = "https://example.test/mcp"
bearer_token = "secret"
`
);
const diagnostics = await getCodexDashboardDiagnostics();
expect(
diagnostics.warnings.some((warning) => warning.includes('does not expose --config overrides'))
).toBe(true);
expect(
diagnostics.warnings.some((warning) => warning.includes('missing from [profiles]'))
).toBe(true);
expect(
diagnostics.warnings.some((warning) => warning.includes('experimental_bearer_token'))
).toBe(true);
expect(diagnostics.warnings.some((warning) => warning.includes('inline bearer_token'))).toBe(
true
);
});
it('saves valid raw config content', async () => {
const result = await saveCodexRawConfig({
rawText: 'model = "gpt-5.4"\n[features]\nmulti_agent = true\n',
});
const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(result.success).toBe(true);
expect(result.mtime).toBeGreaterThan(0);
expect(written).toContain('model = "gpt-5.4"');
});
it('rejects invalid TOML while saving raw config', async () => {
await expect(saveCodexRawConfig({ rawText: 'model = "gpt-5.4"\n[features\n' })).rejects.toThrow(
CodexRawConfigValidationError
);
});
it('rejects stale writes with conflict error', async () => {
const configPath = path.join(codexHome, 'config.toml');
fs.writeFileSync(configPath, 'model = "gpt-5.4"\n');
await expect(
saveCodexRawConfig({
rawText: 'model = "gpt-5.3-codex"\n',
expectedMtime: 1,
})
).rejects.toThrow(CodexRawConfigConflictError);
});
it('rejects writes when expectedMtime differs by even 1ms', async () => {
const configPath = path.join(codexHome, 'config.toml');
fs.writeFileSync(configPath, 'model = "gpt-5.4"\n');
const current = await getCodexRawConfig();
await expect(
saveCodexRawConfig({
rawText: 'model = "gpt-5.4"\nprofile = "work"\n',
expectedMtime: current.mtime + 1,
})
).rejects.toThrow(CodexRawConfigConflictError);
});
it('patches top-level settings and project trust through structured controls', async () => {
const result = await patchCodexConfig({
kind: 'top-level',
values: {
model: 'gpt-5.4',
modelReasoningEffort: 'high',
approvalPolicy: 'never',
sandboxMode: 'workspace-write',
webSearch: 'cached',
toolOutputTokenLimit: 12000,
personality: 'friendly',
},
});
await patchCodexConfig({
kind: 'project-trust',
path: '/tmp/workspace-a',
trustLevel: 'trusted',
expectedMtime: result.mtime,
});
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.config.model).toBe('gpt-5.4');
expect(diagnostics.config.modelReasoningEffort).toBe('high');
expect(diagnostics.config.toolOutputTokenLimit).toBe(12000);
expect(diagnostics.config.personality).toBe('friendly');
expect(diagnostics.config.projectTrust[0]?.path).toBe('/tmp/workspace-a');
expect(result.rawText).toContain('model = "gpt-5.4"');
expect(result.config?.model).toBe('gpt-5.4');
});
it('allows structured patches on existing config.toml even when expectedMtime is omitted', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n');
const result = await patchCodexConfig({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
});
expect(result.rawText).toContain('model = "gpt-5.4"');
expect(result.rawText).toContain('[features]');
expect(result.rawText).toContain('multi_agent = true');
expect(result.config?.features).toEqual({ multi_agent: true });
});
it('preserves unsupported approval_policy objects when structured saves touch other fields', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
'model = "gpt-5.4"\napproval_policy = { granular = { edit = "on-request" } }\n'
);
const current = await getCodexRawConfig();
const result = await patchCodexConfig({
kind: 'top-level',
expectedMtime: current.mtime,
values: {
model: 'gpt-5.4-mini',
approvalPolicy: null,
},
});
expect(result.rawText).toContain('model = "gpt-5.4-mini"');
expect(result.rawText).toContain('[approval_policy.granular]');
expect(result.rawText).toContain('edit = "on-request"');
expect(result.config?.approval_policy).toEqual({
granular: { edit: 'on-request' },
});
});
it('expands home paths for project trust and rejects relative paths', async () => {
const homeWorkspacePath = path.join(os.homedir(), 'codex-workspace');
const expanded = await patchCodexConfig({
kind: 'project-trust',
path: '~/codex-workspace',
trustLevel: 'trusted',
});
expect(expanded.rawText).toContain(`[projects."${homeWorkspacePath}"]`);
await expect(
patchCodexConfig({
kind: 'project-trust',
path: './relative-workspace',
trustLevel: 'trusted',
})
).rejects.toThrow(CodexRawConfigValidationError);
});
it('patches profiles, providers, and mcp servers through structured controls', async () => {
const providerResult = await patchCodexConfig({
kind: 'model-provider',
action: 'upsert',
name: 'cliproxy',
values: {
displayName: 'CLIProxy',
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
envKey: 'CLIPROXY_API_KEY',
wireApi: 'responses',
},
});
const profileResult = await patchCodexConfig({
kind: 'profile',
action: 'upsert',
name: 'deep-review',
values: {
model: 'gpt-5.4',
modelProvider: 'cliproxy',
modelReasoningEffort: 'xhigh',
},
setAsActive: true,
expectedMtime: providerResult.mtime,
});
await patchCodexConfig({
kind: 'mcp-server',
action: 'upsert',
name: 'playwright',
values: {
transport: 'stdio',
command: 'npx',
args: ['@playwright/mcp@latest'],
enabled: true,
required: false,
startupTimeoutSec: 15,
toolTimeoutSec: 30,
},
expectedMtime: profileResult.mtime,
});
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.config.activeProfile).toBe('deep-review');
expect(diagnostics.config.modelProviderCount).toBe(1);
expect(diagnostics.config.mcpServerCount).toBe(1);
const raw = await getCodexRawConfig();
expect(raw.rawText).toContain('[profiles.deep-review]');
expect(raw.rawText).toContain('[model_providers.cliproxy]');
expect(raw.rawText).toContain('[mcp_servers.playwright]');
expect(profileResult.rawText).toContain('[profiles.deep-review]');
expect(profileResult.config?.profile).toBe('deep-review');
});
it('accepts non-integer MCP timeout values documented by upstream Codex', async () => {
const result = await patchCodexConfig({
kind: 'mcp-server',
action: 'upsert',
name: 'streaming',
values: {
transport: 'stdio',
command: 'npx',
startupTimeoutSec: 1.5,
toolTimeoutSec: 2.25,
},
});
expect(result.rawText).toContain('startup_timeout_sec = 1.5');
expect(result.rawText).toContain('tool_timeout_sec = 2.25');
});
it('rewrites legacy startup_timeout_ms keys when editing MCP server timeouts', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
['[mcp_servers.streaming]', 'command = "npx"', 'startup_timeout_ms = 1500', ''].join('\n')
);
const raw = await getCodexRawConfig();
const result = await patchCodexConfig({
kind: 'mcp-server',
action: 'upsert',
name: 'streaming',
expectedMtime: raw.mtime,
values: {
transport: 'stdio',
command: 'npx',
startupTimeoutSec: 2.5,
},
});
expect(result.rawText).toContain('startup_timeout_sec = 2.5');
expect(result.rawText).not.toContain('startup_timeout_ms');
});
it('patches streamable-http mcp servers through structured controls', async () => {
const result = await patchCodexConfig({
kind: 'mcp-server',
action: 'upsert',
name: 'remote',
values: {
transport: 'streamable-http',
url: 'https://example.test/mcp',
enabled: true,
required: true,
toolTimeoutSec: 45,
enabledTools: ['browser_snapshot'],
disabledTools: ['slow_tool'],
},
});
expect(result.rawText).toContain('[mcp_servers.remote]');
expect(result.rawText).toContain('url = "https://example.test/mcp"');
expect(result.rawText).toContain('required = true');
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.config.mcpServers).toEqual([
expect.objectContaining({
name: 'remote',
transport: 'streamable-http',
required: true,
toolTimeoutSec: 45,
enabledToolsCount: 1,
disabledToolsCount: 1,
}),
]);
});
it('rejects structured patches when config.toml is invalid', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n[features\n');
await expect(
patchCodexConfig({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
})
).rejects.toThrow(CodexRawConfigValidationError);
});
it('removes feature overrides when a feature is reset to inherited state', async () => {
const enabled = await patchCodexConfig({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
});
expect(enabled.rawText).toContain('[features]');
expect(enabled.rawText).toContain('multi_agent = true');
const reset = await patchCodexConfig({
kind: 'feature',
feature: 'multi_agent',
enabled: null,
expectedMtime: enabled.mtime,
});
expect(reset.rawText).not.toContain('[features]');
expect(reset.config?.features).toBeUndefined();
});
it('rejects malformed structured patch payloads at runtime', async () => {
await expect(
patchCodexConfig({
kind: 'feature',
feature: 'multi_agent',
enabled: 'true' as unknown as boolean | null,
})
).rejects.toThrow(CodexRawConfigValidationError);
await expect(
patchCodexConfig({
kind: 'project-trust',
path: '~/codex-workspace',
trustLevel: 'always',
})
).rejects.toThrow(CodexRawConfigValidationError);
await expect(
patchCodexConfig({
kind: 'mcp-server',
action: 'upsert',
name: 'remote',
values: {
transport: 'http' as 'stdio' | 'streamable-http',
url: 'https://example.test/mcp',
},
})
).rejects.toThrow(CodexRawConfigValidationError);
});
it('rejects invalid enum values even when they already exist in config.toml', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
'model = "gpt-5.4"\napproval_policy = "legacy"\n'
);
await expect(
patchCodexConfig({
kind: 'top-level',
values: {
approvalPolicy: 'legacy' as unknown as 'on-request' | 'never' | 'untrusted' | null,
},
})
).rejects.toThrow(CodexRawConfigValidationError);
});
});
+170
View File
@@ -0,0 +1,170 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import express from 'express';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
let server: Server;
let baseUrl = '';
let tempDir = '';
let codexHome = '';
let originalCodexHome: string | undefined;
beforeAll(async () => {
originalCodexHome = process.env.CODEX_HOME;
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-routes-test-'));
codexHome = path.join(tempDir, '.codex-home');
process.env.CODEX_HOME = codexHome;
const codexRoutesModule = await import('../../../src/web-server/routes/codex-routes');
const app = express();
app.use(express.json());
app.use('/api/codex', codexRoutesModule.default);
server = app.listen(0, '127.0.0.1');
await new Promise<void>((resolve) => server.on('listening', () => resolve()));
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
baseUrl = `http://127.0.0.1:${address.port}`;
});
beforeEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
fs.mkdirSync(codexHome, { recursive: true });
});
afterAll(async () => {
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
if (originalCodexHome !== undefined) {
process.env.CODEX_HOME = originalCodexHome;
} else {
delete process.env.CODEX_HOME;
}
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('codex routes', () => {
it('returns the current raw config snapshot from PATCH /config/patch', async () => {
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'top-level',
values: {
model: 'gpt-5.4',
sandboxMode: 'workspace-write',
},
}),
});
expect(res.status).toBe(200);
const json = (await res.json()) as {
success: boolean;
exists: boolean;
mtime: number;
rawText: string;
config: Record<string, unknown> | null;
parseError: string | null;
readError: string | null;
};
expect(json.success).toBe(true);
expect(json.exists).toBe(true);
expect(json.mtime).toBeGreaterThan(0);
expect(json.parseError).toBeNull();
expect(json.readError).toBeNull();
expect(json.rawText).toContain('model = "gpt-5.4"');
expect(json.rawText).toContain('sandbox_mode = "workspace-write"');
expect(json.config?.model).toBe('gpt-5.4');
const written = fs.readFileSync(path.join(codexHome, 'config.toml'), 'utf8');
expect(written).toBe(json.rawText);
});
it('returns 409 when PATCH /config/patch receives a stale expectedMtime', async () => {
const configPath = path.join(codexHome, 'config.toml');
fs.writeFileSync(configPath, 'model = "gpt-5.3-codex"\n');
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
expectedMtime: 1,
}),
});
expect(res.status).toBe(409);
const json = (await res.json()) as { error: string; mtime: number };
expect(json.error).toContain('File modified externally.');
expect(json.mtime).toBeGreaterThan(0);
});
it('allows PATCH /config/patch on an existing config.toml without expectedMtime', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model = "gpt-5.4"\n');
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'feature',
feature: 'multi_agent',
enabled: true,
}),
});
expect(res.status).toBe(200);
const json = (await res.json()) as {
success: boolean;
rawText: string;
config: Record<string, unknown> | null;
};
expect(json.success).toBe(true);
expect(json.rawText).toContain('multi_agent = true');
expect(json.config?.features).toEqual({ multi_agent: true });
});
it('returns 400 when PATCH /config/patch omits kind', async () => {
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
const json = (await res.json()) as { error: string };
expect(json.error).toBe('kind is required.');
});
it('returns 400 when PATCH /config/patch receives an invalid trust path', async () => {
const res = await fetch(`${baseUrl}/api/codex/config/patch`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'project-trust',
path: './relative-workspace',
trustLevel: 'trusted',
}),
});
expect(res.status).toBe(400);
const json = (await res.json()) as { error: string };
expect(json.error).toContain('Project path must be absolute');
});
});
@@ -13,8 +13,10 @@ describe('route target parsing', () => {
it('returns null for invalid target values', () => {
expect(parseProfileTarget('glm')).toBeNull();
expect(parseProfileTarget('codex')).toBeNull();
expect(parseProfileTarget('')).toBeNull();
expect(parseVariantTarget('factory')).toBeNull();
expect(parseVariantTarget('codex')).toBeNull();
expect(parseVariantTarget(' ')).toBeNull();
});