mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
- add a Codex adapter, detector, runtime aliases, and compatibility matrix - keep Codex runtime-only while preserving persisted targets for claude and droid - cover Codex launch, reasoning, wrapper detection, and bridge routing regressions Refs #773
367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
/**
|
|
* Unit tests for target resolver
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { resolveTargetType, stripTargetFlag } from '../../../src/targets/target-resolver';
|
|
|
|
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;
|
|
|
|
afterEach(() => {
|
|
process.argv = originalArgv;
|
|
if (originalDroidAliases === undefined) {
|
|
delete process.env.CCS_DROID_ALIASES;
|
|
} else {
|
|
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 {
|
|
process.env.CCS_TARGET_ALIASES = originalTargetAliases;
|
|
}
|
|
|
|
if (originalInternalEntryTarget === undefined) {
|
|
delete process.env.CCS_INTERNAL_ENTRY_TARGET;
|
|
} else {
|
|
process.env.CCS_INTERNAL_ENTRY_TARGET = originalInternalEntryTarget;
|
|
}
|
|
});
|
|
|
|
it('should return claude as default', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType([])).toBe('claude');
|
|
});
|
|
|
|
it('should detect --target flag', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
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');
|
|
});
|
|
|
|
it('should use per-profile config when no flag', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType([], { target: 'droid' })).toBe('droid');
|
|
});
|
|
|
|
it('should fallback to claude when persisted profile target is invalid', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
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');
|
|
});
|
|
|
|
it('should detect ccsd argv[0] (busybox pattern)', () => {
|
|
process.argv = ['node', 'ccsd'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
});
|
|
|
|
it('should detect built-in ccs-droid argv[0] alias', () => {
|
|
process.argv = ['node', 'ccs-droid'];
|
|
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 = 'not-a-target=mystery-codex;droid=ccs-droid-custom';
|
|
process.argv = ['node', 'mystery-codex'];
|
|
expect(resolveTargetType([])).toBe('claude');
|
|
});
|
|
|
|
it('should detect custom argv[0] aliases from CCS_DROID_ALIASES', () => {
|
|
process.env.CCS_DROID_ALIASES = 'droidx,my-droid';
|
|
process.argv = ['node', 'my-droid'];
|
|
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';
|
|
|
|
process.argv = ['node', 'team-droid'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
|
|
process.argv = ['node', 'legacy-droid'];
|
|
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'];
|
|
expect(resolveTargetType([])).toBe('claude');
|
|
});
|
|
|
|
it('should detect internal entry target for dedicated package bin entrypoints', () => {
|
|
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
|
|
process.argv = ['node', 'ccs'];
|
|
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'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
});
|
|
|
|
it('should strip .cmd extension on Windows argv[0]', () => {
|
|
process.argv = ['node', 'ccsd.cmd'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
});
|
|
|
|
it('should strip .cmd extension on built-in explicit alias', () => {
|
|
process.argv = ['node', 'ccs-droid.cmd'];
|
|
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');
|
|
});
|
|
|
|
it('should strip .exe extension on Windows argv[0]', () => {
|
|
process.argv = ['node', 'ccsd.exe'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
});
|
|
|
|
it('should handle full path argv[0]', () => {
|
|
process.argv = ['node', '/usr/local/bin/ccsd'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
});
|
|
|
|
it('should handle full path argv[0] for ccs-droid', () => {
|
|
process.argv = ['node', '/usr/local/bin/ccs-droid'];
|
|
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');
|
|
});
|
|
|
|
it('should prioritize --target over internal entry target', () => {
|
|
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType(['--target', 'claude'])).toBe('claude');
|
|
});
|
|
|
|
it('should prioritize runtime alias over profile config', () => {
|
|
process.argv = ['node', 'ccsd'];
|
|
expect(resolveTargetType([], { target: 'claude' })).toBe('droid');
|
|
});
|
|
|
|
it('should prioritize internal entry target over profile config', () => {
|
|
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType([], { target: 'claude' })).toBe('droid');
|
|
});
|
|
|
|
it('should keep reserved command names authoritative', () => {
|
|
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');
|
|
|
|
process.argv = ['node', 'ccs-droid'];
|
|
expect(resolveTargetType([])).toBe('droid');
|
|
|
|
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', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(() => resolveTargetType(['--target', 'invalid'])).toThrow(/Unknown target "invalid"/);
|
|
});
|
|
|
|
it('should support --target=<value> form', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType(['--target=droid'])).toBe('droid');
|
|
});
|
|
|
|
it('should throw when --target is missing value', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(() => resolveTargetType(['--target'])).toThrow(/--target requires a value/);
|
|
});
|
|
|
|
it('should throw when --target value is another flag', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(() => resolveTargetType(['--target', '--help'])).toThrow(/--target requires a value/);
|
|
});
|
|
|
|
it('should use last --target flag when repeated', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType(['--target', 'droid', '--target=claude'])).toBe('claude');
|
|
});
|
|
|
|
it('should ignore --target after option terminator', () => {
|
|
process.argv = ['node', 'ccs'];
|
|
expect(resolveTargetType(['--', '--target', 'droid'])).toBe('claude');
|
|
});
|
|
});
|
|
|
|
describe('stripTargetFlag', () => {
|
|
it('should remove --target and its value', () => {
|
|
expect(stripTargetFlag(['gemini', '--target', 'droid'])).toEqual(['gemini']);
|
|
});
|
|
|
|
it('should handle --target at start', () => {
|
|
expect(stripTargetFlag(['--target', 'droid', 'gemini'])).toEqual(['gemini']);
|
|
});
|
|
|
|
it('should return args unchanged if no --target', () => {
|
|
const args = ['gemini', '-p', 'hello'];
|
|
expect(stripTargetFlag(args)).toEqual(['gemini', '-p', 'hello']);
|
|
});
|
|
|
|
it('should remove --target=<value> form', () => {
|
|
expect(stripTargetFlag(['gemini', '--target=droid', '--verbose'])).toEqual([
|
|
'gemini',
|
|
'--verbose',
|
|
]);
|
|
});
|
|
|
|
it('should remove repeated --target flags', () => {
|
|
expect(
|
|
stripTargetFlag(['--target', 'droid', 'gemini', '--target=claude', '--verbose'])
|
|
).toEqual(['gemini', '--verbose']);
|
|
});
|
|
|
|
it('should throw when --target has no value', () => {
|
|
expect(() => stripTargetFlag(['gemini', '--target'])).toThrow(/--target requires a value/);
|
|
});
|
|
|
|
it('should throw when --target value is another flag', () => {
|
|
expect(() => stripTargetFlag(['gemini', '--target', '--help'])).toThrow(
|
|
/--target requires a value/
|
|
);
|
|
});
|
|
|
|
it('should not modify the original array', () => {
|
|
const args = ['--target', 'droid', 'gemini'];
|
|
stripTargetFlag(args);
|
|
expect(args).toEqual(['--target', 'droid', 'gemini']);
|
|
});
|
|
|
|
it('should preserve args after option terminator', () => {
|
|
expect(stripTargetFlag(['glm', '--', '--target', 'droid', '-p'])).toEqual([
|
|
'glm',
|
|
'--',
|
|
'--target',
|
|
'droid',
|
|
'-p',
|
|
]);
|
|
});
|
|
});
|