mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
* feat(cliproxy): add HTTPS tunnel for remote proxy mode Claude Code doesn't support HTTPS in ANTHROPIC_BASE_URL directly (undici limitation). This adds an HTTP→HTTPS tunnel proxy for remote CLIProxyAPI connections. Changes: - Add HttpsTunnelProxy: local HTTP server tunneling to remote HTTPS - Add CodexReasoningProxy HTTPS support and path prefix stripping - Remote mode now uses root paths (/v1/messages) not provider-prefixed - Add remote token uploader for syncing OAuth tokens to remote server - Auto-upload tokens after OAuth auth when remote mode is enabled Flow for remote HTTPS: Claude CLI → CodexReasoningProxy → HttpsTunnel → Remote HTTPS Built with [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> * fix: address PR review issues for HTTPS tunnel proxy - Add connection tracking with activeConnections Set for proper cleanup - Add port validation after start() to reject port 0 - Add Authorization header fallback injection in buildForwardHeaders() - Handle client disconnect (premature close) and request errors - Improve error handling in uploadTokenToRemoteAsync (log instead of silent catch) - Add comprehensive tests for HttpsTunnelProxy (97% coverage) - Add integration tests for remote-token-uploader - Add stripPathPrefix unit tests for remote mode Built with [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> * fix: bump Node.js engine requirement to >=18.0.0 FormData and Blob APIs used in remote-token-uploader require Node.js 18+. Addresses coderabbit review comment about Node.js engine compatibility. Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> * fix: address remaining PR suggestions - Add verbose parameter to registerAccountFromToken for proper propagation - Improve stripPathPrefix with path normalization (double slashes, leading slash) - Add edge case tests for path normalization Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> * fix: add timeout to flaky npm CLI tests Tests 'handles empty arguments gracefully' and 'handles very long argument' were missing timeout option in execSync, causing occasional timeouts when bun test's 5000ms limit was reached before CLI completed. Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> * fix: address new PR review feedback - Add path segment boundary check to prevent partial matches (/codex vs /codextra) - Add hostname validation in HttpsTunnelProxy constructor - Add race condition protection with 'starting' flag in start() - Sanitize error messages (detailed only in verbose mode) - Update comments for clarity (regex behavior) - Add comprehensive tests for all edge cases Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai> --------- Co-authored-by: OnSteroids <built@onsteroids.ai>
164 lines
6.0 KiB
JavaScript
164 lines
6.0 KiB
JavaScript
const assert = require('assert');
|
|
const { execSync } = require('child_process');
|
|
const path = require('path');
|
|
const { createTestEnvironment } = require('../shared/fixtures/test-environment');
|
|
|
|
describe('npm CLI', () => {
|
|
const ccsPath = path.join(__dirname, '..', '..', 'dist', 'ccs.js');
|
|
let testEnv;
|
|
let testCcsHome;
|
|
|
|
beforeAll(() => {
|
|
// Create isolated test environment
|
|
testEnv = createTestEnvironment();
|
|
testCcsHome = testEnv.testHome;
|
|
|
|
// Run postinstall to create config in test environment
|
|
const postinstallScript = path.join(__dirname, '..', '..', 'scripts', 'postinstall.js');
|
|
execSync(`node "${postinstallScript}"`, {
|
|
stdio: 'ignore',
|
|
env: { ...process.env, CCS_HOME: testCcsHome }
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Clean up test environment
|
|
if (testEnv) {
|
|
testEnv.cleanup();
|
|
}
|
|
});
|
|
|
|
// Helper to run CLI with test environment
|
|
function runCli(args, options = {}) {
|
|
return execSync(`node "${ccsPath}" ${args}`, {
|
|
...options,
|
|
env: { ...process.env, CCS_HOME: testCcsHome }
|
|
});
|
|
}
|
|
|
|
describe('Argument parsing', () => {
|
|
it('handles flag -c without profile error', function() {
|
|
try {
|
|
runCli('-c', { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
// Should NOT show "Profile '-c' not found" error
|
|
assert(!output.includes("Profile '-c' not found"), 'Should not treat -c as profile');
|
|
}
|
|
});
|
|
|
|
it('handles flag --verbose without profile error', function() {
|
|
try {
|
|
runCli('--verbose', { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(!output.includes("Profile '--verbose' not found"), 'Should not treat --verbose as profile');
|
|
}
|
|
});
|
|
|
|
it('handles flag -p with value', function() {
|
|
try {
|
|
runCli('-p "test prompt"', { stdio: 'pipe', timeout: 8000 });
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(!output.includes("Profile '-p' not found"), 'Should not treat -p as profile');
|
|
}
|
|
});
|
|
|
|
it('handles multiple flags', function() {
|
|
try {
|
|
runCli('-c --verbose', { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(!output.includes("Profile '-c' not found"), 'Should not treat flags as profiles');
|
|
assert(!output.includes("Profile '--verbose' not found"), 'Should not treat flags as profiles');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Profile handling', () => {
|
|
// Note: GLM/GLMT/Kimi profiles are no longer auto-created (v6.0)
|
|
// Users create these via UI presets or CLI: ccs api create --preset glm
|
|
|
|
it('shows helpful error for non-existent profile', function() {
|
|
try {
|
|
runCli('glm --help', { stdio: 'pipe' });
|
|
// If GLM profile exists from previous setup, this is fine too
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
// Either profile exists and works, or shows helpful "not found" message
|
|
// Both are valid behaviors depending on user's setup
|
|
const isValid = !output.includes("Profile 'glm' not found") ||
|
|
output.includes("not found") ||
|
|
output.includes("ccs api create");
|
|
assert(isValid, 'Should either find profile or show helpful message');
|
|
}
|
|
});
|
|
|
|
it('shows error for invalid profile', function() {
|
|
try {
|
|
runCli('invalid-profile-name', { stdio: 'pipe' });
|
|
assert(false, 'Should have thrown an error for invalid profile');
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(output.includes("not found") || output.includes("invalid"), 'Should show profile not found error');
|
|
}
|
|
});
|
|
|
|
it('handles profile with flags correctly', function() {
|
|
try {
|
|
// Use a known command instead of profile that may not exist
|
|
runCli('api --help', { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
const output = e.stderr?.toString() || '';
|
|
assert(!output.includes("Profile '-c' not found"), 'Should not treat flags as profiles');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Version and help', () => {
|
|
it('shows version with --version flag', function() {
|
|
const output = runCli('--version', { encoding: 'utf8' });
|
|
assert(/\d+\.\d+\.\d+/.test(output), 'Should show version number');
|
|
});
|
|
|
|
it('shows version with -v flag', function() {
|
|
const output = runCli('-v', { encoding: 'utf8' });
|
|
assert(/\d+\.\d+\.\d+/.test(output), 'Should show version number');
|
|
});
|
|
|
|
it('shows help with --help flag', function() {
|
|
const output = runCli('--help', { encoding: 'utf8' });
|
|
assert(/usage|help|options/i.test(output), 'Should show help information');
|
|
});
|
|
|
|
it('shows help with -h flag', function() {
|
|
const output = runCli('-h', { encoding: 'utf8' });
|
|
assert(/usage|help|options/i.test(output), 'Should show help information');
|
|
});
|
|
});
|
|
|
|
describe('Error handling', () => {
|
|
it('handles empty arguments gracefully', function() {
|
|
try {
|
|
runCli('', { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
// Should either succeed or fail gracefully with a helpful error
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(!output.includes('TypeError') && !output.includes('Cannot read'), 'Should not crash with TypeError');
|
|
}
|
|
});
|
|
|
|
it('handles very long argument', function() {
|
|
const longArg = 'a'.repeat(1000);
|
|
try {
|
|
runCli(`"${longArg}"`, { stdio: 'pipe', timeout: 3000 });
|
|
} catch (e) {
|
|
// Should handle gracefully, not crash
|
|
const output = e.stderr?.toString() || e.stdout?.toString() || '';
|
|
assert(!output.includes('TypeError') && !output.includes('Cannot read'), 'Should not crash with TypeError');
|
|
}
|
|
});
|
|
});
|
|
});
|