mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 16:16:52 +00:00
* fix(cliproxy): pass variant port to executor for isolation Variants configured with dedicated ports (8318-8417) were not using their assigned port. The executor always defaulted to 8317. Changes: - Add port field to ProfileDetectionResult interface - Pass variant.port from profile-detector to ccs.ts - Forward port to execClaudeWithCLIProxy options - Update executor priority: CLI flags > variant port > config.yaml > default Closes #228 * fix(cliproxy): propagate port in unified config and UI preset handlers Edge case fixes identified in codebase review: - Unified config variant detection: add settingsPath and port fields - Provider editor: use variant port in handleApplyPreset/handleCustomPresetApply - preset-utils: add optional port parameter to applyDefaultPreset() * chore(release): 7.11.1-dev.1 [skip ci] * fix(cliproxy): use correct default port (8317) for remote HTTP connections Root cause: Inconsistent default port logic across code paths. - Test Connection used 8317 (correct) - Actual API calls used 80 (wrong) Changes: - Add centralized getRemoteDefaultPort() helper in config-generator.ts - Fix proxy-target-resolver.ts to use shared helper - Fix rewriteLocalhostUrls() and getRemoteEnvVars() in config-generator.ts - Update remote-proxy-client.ts to use shared helper (DRY) Fixes all 3 reported issues: 1. Port empty → now correctly uses :8317 instead of :80 2. BASEURL construction → now includes correct port 3. CLIProxy Plus auth → now fetches from remote on correct port * chore(release): 7.11.1-dev.2 [skip ci] * feat(delegation): add Claude Code CLI flag passthrough Add explicit passthrough support for key Claude Code CLI flags: - --max-turns: Limit agentic turns (prevents infinite loops) - --fallback-model: Auto-fallback when model overloaded - --agents: Dynamic subagent JSON injection - --betas: Enable experimental features Maintain extraArgs catch-all for future Claude Code flags. Update help command with new "Delegation Flags" section. Closes #89 * test(delegation): add comprehensive CLI flag passthrough tests Add 45 test cases covering all edge cases for CLI flag passthrough: - DelegationHandler: timeout/max-turns/fallback-model/agents/betas validation - HeadlessExecutor: duplicate flag filtering, undefined vs truthy checks * chore(release): 7.11.1-dev.3 [skip ci] * fix(ui): enable cancel button during OAuth authentication Resolves #234 - Cancel button was disabled during authentication flow, preventing users from canceling the OAuth process. Changes: - Add auth-session-manager.ts for tracking active OAuth sessions - Add POST /cliproxy/auth/:provider/cancel endpoint to abort sessions - Kill spawned CLIProxy auth process when cancel is triggered - Enable Cancel button in AddAccountDialog during authentication - Add cancel support to QuickSetupWizard auth step - Update useCancelAuth hook to call backend cancel endpoint * chore(release): 7.11.1-dev.4 [skip ci] * fix(prompt): add stdin.pause() to prevent process hang after password input Fixes #236. The password() method called resume() on stdin but never paused it in cleanup, keeping the event loop alive indefinitely. * chore(release): 7.11.1-dev.5 [skip ci] * feat(cliproxy): add --allow-self-signed flag for HTTPS connections (#227) Previously, allowSelfSigned was hardcoded to true for all HTTPS protocol connections, forcing use of the native https module which has issues with Cloudflare-proxied connections causing timeouts. This change: - Adds --allow-self-signed CLI flag (default: false) - Adds CCS_ALLOW_SELF_SIGNED environment variable - Uses standard fetch API by default for HTTPS (works with valid certs) - Only uses native https module when --allow-self-signed is specified Usage: - For production HTTPS proxies with valid certs: no flag needed - For dev proxies with self-signed certs: use --allow-self-signed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 7.11.1-dev.6 [skip ci] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Shun Kakinoki <39187513+shunkakinoki@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
/**
|
|
* Tests for delegation-handler CLI flag passthrough feature
|
|
* Covers: parseStringFlag, timeout validation, max-turns validation, agents JSON validation
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test';
|
|
|
|
// Import the DelegationHandler class
|
|
import { DelegationHandler } from '../../../src/delegation/delegation-handler';
|
|
|
|
describe('DelegationHandler', () => {
|
|
let handler: DelegationHandler;
|
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
|
|
|
beforeEach(() => {
|
|
handler = new DelegationHandler();
|
|
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
|
|
describe('_extractOptions - timeout validation', () => {
|
|
it('accepts valid positive timeout', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '30000']);
|
|
expect(options.timeout).toBe(30000);
|
|
});
|
|
|
|
it('rejects NaN timeout with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', 'abc']);
|
|
expect(options.timeout).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects negative timeout with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '-5000']);
|
|
expect(options.timeout).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects zero timeout with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '0']);
|
|
expect(options.timeout).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects timeout exceeding max (600000ms) with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout', '700000']);
|
|
expect(options.timeout).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores missing timeout value at end of args', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--timeout']);
|
|
expect(options.timeout).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('_extractOptions - max-turns validation', () => {
|
|
it('accepts valid positive max-turns', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '10']);
|
|
expect(options.maxTurns).toBe(10);
|
|
});
|
|
|
|
it('rejects NaN max-turns with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', 'abc']);
|
|
expect(options.maxTurns).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects negative max-turns with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '-5']);
|
|
expect(options.maxTurns).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects zero max-turns with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '0']);
|
|
expect(options.maxTurns).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('caps max-turns at 100 when exceeding limit', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '500']);
|
|
expect(options.maxTurns).toBe(100);
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('accepts max-turns at exactly 100', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--max-turns', '100']);
|
|
expect(options.maxTurns).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe('_extractOptions - fallback-model validation', () => {
|
|
it('accepts valid fallback-model', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model', 'sonnet']);
|
|
expect(options.fallbackModel).toBe('sonnet');
|
|
});
|
|
|
|
it('rejects dash-prefixed value with warning', () => {
|
|
const options = handler._extractOptions([
|
|
'glm',
|
|
'-p',
|
|
'test',
|
|
'--fallback-model',
|
|
'--other-flag',
|
|
]);
|
|
expect(options.fallbackModel).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects empty string value', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model', '']);
|
|
expect(options.fallbackModel).toBeUndefined();
|
|
});
|
|
|
|
it('rejects whitespace-only value', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--fallback-model', ' ']);
|
|
expect(options.fallbackModel).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('_extractOptions - agents JSON validation', () => {
|
|
it('accepts valid JSON for agents', () => {
|
|
const options = handler._extractOptions([
|
|
'glm',
|
|
'-p',
|
|
'test',
|
|
'--agents',
|
|
'{"name":"test"}',
|
|
]);
|
|
expect(options.agents).toBe('{"name":"test"}');
|
|
});
|
|
|
|
it('rejects invalid JSON with warning', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '{invalid json}']);
|
|
expect(options.agents).toBeUndefined();
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects dash-prefixed value', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '--other']);
|
|
expect(options.agents).toBeUndefined();
|
|
});
|
|
|
|
it('accepts JSON array for agents', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '[{"a":1}]']);
|
|
expect(options.agents).toBe('[{"a":1}]');
|
|
});
|
|
});
|
|
|
|
describe('_extractOptions - betas validation', () => {
|
|
it('accepts valid betas value', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--betas', 'feature1,feature2']);
|
|
expect(options.betas).toBe('feature1,feature2');
|
|
});
|
|
|
|
it('rejects dash-prefixed value', () => {
|
|
const options = handler._extractOptions(['glm', '-p', 'test', '--betas', '--feature']);
|
|
expect(options.betas).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('_extractOptions - extraArgs passthrough', () => {
|
|
it('passes unknown flags through to extraArgs', () => {
|
|
const options = handler._extractOptions([
|
|
'glm',
|
|
'-p',
|
|
'test',
|
|
'--unknown-flag',
|
|
'value',
|
|
]);
|
|
expect(options.extraArgs).toContain('--unknown-flag');
|
|
expect(options.extraArgs).toContain('value');
|
|
});
|
|
|
|
it('excludes CCS-handled flags from extraArgs', () => {
|
|
const options = handler._extractOptions([
|
|
'glm',
|
|
'-p',
|
|
'test',
|
|
'--max-turns',
|
|
'10',
|
|
'--unknown',
|
|
'val',
|
|
]);
|
|
expect(options.extraArgs).not.toContain('--max-turns');
|
|
expect(options.extraArgs).not.toContain('10');
|
|
expect(options.extraArgs).toContain('--unknown');
|
|
});
|
|
});
|
|
|
|
describe('_extractProfile', () => {
|
|
it('extracts profile name from first non-flag arg', () => {
|
|
const profile = handler._extractProfile(['glm', '-p', 'test']);
|
|
expect(profile).toBe('glm');
|
|
});
|
|
|
|
it('returns empty string when no profile found', () => {
|
|
const profile = handler._extractProfile(['-p', 'test']);
|
|
expect(profile).toBe('');
|
|
});
|
|
|
|
it('skips flag values correctly', () => {
|
|
const profile = handler._extractProfile(['-p', 'test', 'kimi']);
|
|
expect(profile).toBe('kimi');
|
|
});
|
|
});
|
|
});
|