Merge remote-tracking branch 'origin/dev' into pr-689-anthropic-direct-api

# Conflicts:
#	ui/src/components/profiles/profile-dialog.tsx
This commit is contained in:
Tam Nhu Tran committed 2026-03-07 15:59:44 +07:00
commit 5bcad30762
89 files changed
+6407 -726

No files matched your search

@@ -0,0 +1,203 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import { spawn } from 'child_process';
import * as fs from 'fs';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
const HOOK_PATH = path.join(__dirname, '../../lib/hooks/image-analyzer-transformer.cjs');
const TEST_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-image-hook-it-'));
const TEST_PNG_PATH = path.join(TEST_DIR, 'thinking-block-test.png');
const CLIPROXY_API_KEY = 'test-api-key-12345';
interface HookResult {
code: number;
stdout: string;
stderr: string;
}
interface MockRequest {
method: string;
path: string;
body: unknown;
}
interface MockResponse {
statusCode: number;
body: unknown;
}
let mockServer: http.Server | null = null;
let mockPort = 0;
let requests: MockRequest[] = [];
let queuedResponses: MockResponse[] = [];
function createTestPng(filepath: string): void {
const png = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8,
0xcf, 0xc0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb4, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]);
fs.writeFileSync(filepath, png);
}
function enqueueResponses(...responses: MockResponse[]): void {
queuedResponses = responses;
}
function invokeHook(env: Record<string, string> = {}): Promise<HookResult> {
return new Promise((resolve, reject) => {
const child = spawn('node', [HOOK_PATH], {
env: {
...process.env,
CCS_CLIPROXY_API_KEY: CLIPROXY_API_KEY,
CCS_CLIPROXY_PORT: String(mockPort),
CCS_IMAGE_ANALYSIS_ENABLED: '1',
CCS_PROFILE_TYPE: 'cliproxy',
CCS_CURRENT_PROVIDER: 'codex',
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'codex:gpt-5.1-codex-mini,agy:gemini-2.5-flash',
...env,
},
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
const timer = setTimeout(() => {
child.kill('SIGKILL');
reject(new Error('Hook timed out'));
}, 10000);
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('error', (err) => {
clearTimeout(timer);
reject(err);
});
child.on('close', (code) => {
clearTimeout(timer);
resolve({ code: code ?? -1, stdout, stderr });
});
child.stdin.end(
JSON.stringify({
tool_name: 'Read',
tool_input: { file_path: TEST_PNG_PATH },
})
);
});
}
beforeAll(async () => {
createTestPng(TEST_PNG_PATH);
mockServer = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
requests.push({
method: req.method || 'GET',
path: req.url || '/',
body: body ? JSON.parse(body) : null,
});
const nextResponse = queuedResponses.shift() || {
statusCode: 200,
body: { content: [{ type: 'text', text: 'default description' }] },
};
res.writeHead(nextResponse.statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(nextResponse.body));
});
});
await new Promise<void>((resolve, reject) => {
mockServer?.once('error', reject);
mockServer?.listen(0, '127.0.0.1', () => {
const address = mockServer?.address();
if (!address || typeof address === 'string') {
reject(new Error('Failed to resolve mock server port'));
return;
}
mockPort = address.port;
resolve();
});
});
});
beforeEach(() => {
requests = [];
enqueueResponses({
statusCode: 200,
body: { content: [{ type: 'text', text: 'default description' }] },
});
});
afterAll(async () => {
await new Promise<void>((resolve) => mockServer?.close(() => resolve()));
fs.rmSync(TEST_DIR, { recursive: true, force: true });
});
describe('image analyzer hook regression coverage', () => {
it('uses the first text block after thinking blocks', async () => {
enqueueResponses({
statusCode: 200,
body: {
content: [
{ type: 'thinking', thinking: 'internal reasoning' },
{ type: 'text', text: 'blue square with white border' },
],
},
});
const result = await invokeHook();
expect(result.code).toBe(2);
expect(requests).toHaveLength(1);
const output = JSON.parse(result.stdout);
expect(output.hookSpecificOutput.permissionDecisionReason).toContain(
'blue square with white border'
);
});
it('does not retry onto a cross-provider default model when a provider-specific model is set', async () => {
enqueueResponses({
statusCode: 500,
body: { error: { message: 'mock failure' } },
});
const result = await invokeHook();
expect(result.code).toBe(2);
expect(requests).toHaveLength(1);
expect((requests[0].body as { model: string }).model).toBe('gpt-5.1-codex-mini');
});
it('skips analysis before contacting CLIProxy when the current provider has no mapped vision model', async () => {
const result = await invokeHook({
CCS_CURRENT_PROVIDER: 'unknown-provider',
CCS_IMAGE_ANALYSIS_PROVIDER_MODELS: 'agy:gemini-2.5-flash',
});
expect(result.code).toBe(0);
expect(requests).toHaveLength(0);
});
});
@@ -0,0 +1,163 @@
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 {
copyApiProfile,
discoverApiProfileOrphans,
exportApiProfile,
importApiProfileBundle,
registerApiProfileOrphans,
} from '../../../src/api/services/profile-lifecycle-service';
describe('profile lifecycle service', () => {
let tempHome = '';
let originalCcsHome: string | undefined;
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-lifecycle-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('discovers only API profile orphans (skips registered and reserved names)', () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.json'),
JSON.stringify({ profiles: { glm: '~/.ccs/glm.settings.json' } }, null, 2) + '\n'
);
fs.writeFileSync(
path.join(ccsDir, 'glm.settings.json'),
JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } }, null, 2) +
'\n'
);
fs.writeFileSync(
path.join(ccsDir, 'extra.settings.json'),
JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } }, null, 2) +
'\n'
);
fs.writeFileSync(
path.join(ccsDir, 'gemini.settings.json'),
JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } }, null, 2) +
'\n'
);
const result = discoverApiProfileOrphans();
expect(result.orphans.map((orphan) => orphan.name)).toEqual(['extra']);
});
it('treats explicit empty names list as no-op during orphan registration', () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'lonely.settings.json'),
JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } }, null, 2) +
'\n'
);
fs.writeFileSync(path.join(ccsDir, 'config.json'), JSON.stringify({ profiles: {} }, null, 2) + '\n');
const result = registerApiProfileOrphans({ names: [] });
expect(result.registered).toEqual([]);
expect(result.skipped).toEqual([]);
});
it('redacts all sensitive env values during export when includeSecrets=false', () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.json'),
JSON.stringify({ profiles: { glm: '~/.ccs/glm.settings.json' } }, null, 2) + '\n'
);
fs.writeFileSync(
path.join(ccsDir, 'glm.settings.json'),
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'https://api.example.com',
ANTHROPIC_AUTH_TOKEN: 'token-1',
OPENROUTER_API_KEY: 'token-2',
},
},
null,
2
) + '\n'
);
const result = exportApiProfile('glm', false);
expect(result.success).toBe(true);
expect(result.bundle?.settings).toBeDefined();
const env = (result.bundle?.settings.env as Record<string, unknown>) || {};
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('__CCS_REDACTED__');
expect(env.OPENROUTER_API_KEY).toBe('__CCS_REDACTED__');
});
it('rejects invalid source profile names in copy flow', () => {
const result = copyApiProfile('../escape', 'safe-name');
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid source profile name');
});
it('rejects import bundle with invalid profile target', () => {
const result = importApiProfileBundle({
schemaVersion: 1,
exportedAt: new Date().toISOString(),
profile: { name: 'glm', target: 'invalid-target' },
settings: {
env: {
ANTHROPIC_BASE_URL: 'https://api.example.com',
ANTHROPIC_AUTH_TOKEN: 'token',
},
},
});
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid bundle profile target');
});
it('clears and warns for all redacted sensitive env keys on import', () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(path.join(ccsDir, 'config.json'), JSON.stringify({ profiles: {} }, null, 2) + '\n');
const result = importApiProfileBundle({
schemaVersion: 1,
exportedAt: new Date().toISOString(),
profile: { name: 'redacted-import', target: 'claude' },
settings: {
env: {
ANTHROPIC_BASE_URL: 'https://api.example.com',
ANTHROPIC_AUTH_TOKEN: '__CCS_REDACTED__',
OPENROUTER_API_KEY: '__CCS_REDACTED__',
},
},
});
expect(result.success).toBe(true);
expect(result.warnings?.length).toBeGreaterThan(0);
const settingsPath = path.join(ccsDir, 'redacted-import.settings.json');
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
env: Record<string, string>;
};
expect(parsed.env.ANTHROPIC_AUTH_TOKEN).toBe('');
expect(parsed.env.OPENROUTER_API_KEY).toBe('');
});
});
+16
View File
@@ -47,6 +47,22 @@ describe('auth command args parsing', () => {
expect(parsed.deeperContinuity).toBe(true);
});
it('parses bare flag for create command', () => {
const parsed = parseArgs(['work', '--bare']);
expect(parsed.profileName).toBe('work');
expect(parsed.bare).toBe(true);
});
it('parses bare flag with shared context flags', () => {
const parsed = parseArgs(['work', '--bare', '--share-context', '--context-group', 'sprint-a']);
expect(parsed.profileName).toBe('work');
expect(parsed.bare).toBe(true);
expect(parsed.shareContext).toBe(true);
expect(parsed.contextGroup).toBe('sprint-a');
});
it('tracks unknown flags and keeps positional profile intact', () => {
const parsed = parseArgs(['--foo', 'bar', 'work']);
@@ -61,7 +61,7 @@ describe('resolveProfileContinuityInheritance', () => {
mode: 'shared',
group: 'team-alpha',
continuityMode: 'deeper',
});
}, { bare: false });
});
it('supports legacy continuity_inherit_from_account fallback', async () => {
@@ -255,7 +255,46 @@ describe('resolveProfileContinuityInheritance', () => {
});
expect(ensureInstanceSpy).toHaveBeenCalledWith('pro', {
mode: 'isolated',
}, { bare: false });
});
it('propagates bare source-account mode when inheriting continuity', async () => {
spyOn(configLoader, 'loadOrCreateUnifiedConfig').mockReturnValue({
version: 8,
continuity: {
inherit_from_account: {
glm: 'pro',
},
},
} as ReturnType<typeof configLoader.loadOrCreateUnifiedConfig>);
const ensureInstanceSpy = spyOn(InstanceManager.prototype, 'ensureInstance').mockResolvedValue(
'/tmp/.ccs/instances/pro'
);
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
pro: {
type: 'account',
created: '2026-03-01T00:00:00.000Z',
last_used: null,
bare: true,
},
});
const result = await resolveProfileContinuityInheritance({
profileName: 'glm',
profileType: 'settings',
target: 'claude',
});
expect(result).toEqual({
sourceAccount: 'pro',
claudeConfigDir: '/tmp/.ccs/instances/pro',
});
expect(ensureInstanceSpy).toHaveBeenCalledWith(
'pro',
{ mode: 'isolated' },
{ bare: true }
);
});
it('does not apply km settings alias mapping to kimi cliproxy profile', async () => {
+2 -1
View File
@@ -161,7 +161,7 @@ describe('ProfileDetector', () => {
const mockUnifiedConfig = {
version: 2,
accounts: {
work: { created: '2025-01-01', last_used: '2025-01-02' },
work: { created: '2025-01-01', last_used: '2025-01-02', bare: true },
},
};
@@ -176,6 +176,7 @@ describe('ProfileDetector', () => {
expect(result.name).toBe('work');
expect(result.profile).toBeDefined();
expect((result.profile as any).type).toBe('account');
expect((result.profile as any).bare).toBe(true);
} finally {
isUnifiedModeSpy.mockRestore();
loadUnifiedConfigSpy.mockRestore();
@@ -91,4 +91,43 @@ describe('profile-registry context normalization', () => {
expect(accounts.work.context_group).toBeUndefined();
expect(accounts.work.continuity_mode).toBe('standard');
});
it('persists bare flag for legacy profiles', () => {
const registry = new ProfileRegistry();
registry.createProfile('work', { type: 'account', bare: true });
const profile = registry.getProfile('work');
expect(profile.bare).toBe(true);
});
it('persists bare flag for unified accounts and merged projection', () => {
process.env.CCS_UNIFIED_CONFIG = '1';
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'config.yaml'),
[
'version: 8',
'accounts:',
' work:',
' created: "2026-03-05T00:00:00.000Z"',
' last_used: null',
' bare: true',
'profiles: {}',
'cliproxy:',
' oauth_accounts: {}',
' providers: {}',
' variants: {}',
].join('\n'),
'utf8'
);
const registry = new ProfileRegistry();
const accounts = registry.getAllAccountsUnified();
expect(accounts.work.bare).toBe(true);
const merged = registry.getAllProfilesMerged();
expect(merged.work.bare).toBe(true);
});
});
@@ -9,7 +9,7 @@
* - Email masking
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -18,18 +18,22 @@ import {
writeQuotaWarning,
maskEmail,
} from '../../../src/cliproxy/account-safety';
import { sanitizeEmail } from '../../../src/cliproxy/auth-utils';
// Setup test isolation
let tmpDir: string;
let origCcsHome: string | undefined;
let originalFetch: typeof fetch;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-test-exhaust-'));
origCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpDir;
originalFetch = global.fetch;
});
afterEach(() => {
global.fetch = originalFetch;
if (origCcsHome !== undefined) {
process.env.CCS_HOME = origCcsHome;
} else {
@@ -61,6 +65,25 @@ function writeConfig(quotaConfig: unknown): void {
);
}
function writeClaudeAuth(accountId: string, accessToken: string): void {
const authDir = path.join(tmpDir, '.ccs', 'cliproxy', 'auth');
const tokenFile = `claude-${sanitizeEmail(accountId)}.json`;
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(
path.join(authDir, tokenFile),
JSON.stringify(
{
access_token: accessToken,
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
email: accountId,
},
null,
2
)
);
}
describe('Quota Exhaustion Handlers', () => {
describe('writeQuotaWarning', () => {
it('should write to stderr with box format', async () => {
@@ -230,6 +253,69 @@ describe('Quota Exhaustion Handlers', () => {
expect(result.reason).toContain('no alternatives');
});
it('should switch Claude accounts when fallback quota is unavailable but auth is valid', async () => {
writeRegistry({
claude: {
default: 'exhausted@example.com',
accounts: {
'exhausted@example.com': {
email: 'exhausted@example.com',
tokenFile: `claude-${sanitizeEmail('exhausted@example.com')}.json`,
},
'fallback@example.com': {
email: 'fallback@example.com',
tokenFile: `claude-${sanitizeEmail('fallback@example.com')}.json`,
},
},
},
});
writeConfig({
mode: 'auto',
auto: {
tier_priority: ['ultra', 'pro', 'free'],
exhaustion_threshold: 5,
cooldown_minutes: 10,
preflight_check: true,
},
runtime_monitor: {
enabled: true,
normal_interval_seconds: 300,
critical_interval_seconds: 60,
warn_threshold: 20,
exhaustion_threshold: 5,
cooldown_minutes: 10,
},
});
writeClaudeAuth('exhausted@example.com', 'exhausted-token');
writeClaudeAuth('fallback@example.com', 'fallback-token');
global.fetch = mock((_url: string, options?: RequestInit) => {
const authHeader = new Headers(options?.headers).get('Authorization') ?? '';
if (authHeader === 'Bearer fallback-token') {
return Promise.resolve(
new Response(
JSON.stringify({
error: {
message: 'OAuth authentication is currently not supported.',
},
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
);
}
return Promise.resolve(new Response('', { status: 500 }));
}) as typeof fetch;
const result = await handleQuotaExhaustion('claude', 'exhausted@example.com', 10);
const { getDefaultAccount } = await import('../../../src/cliproxy/account-manager');
expect(result.switchedTo).toBe('fallback@example.com');
expect(getDefaultAccount('claude')?.id).toBe('fallback@example.com');
});
it('should write warning to stderr', async () => {
writeRegistry({
agy: {
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'bun:test';
import {
getManagementAuthUrlPath,
getPasteCallbackStartPath,
getManagementOAuthCallbackPath,
} from '../../../src/cliproxy/auth/auth-types';
describe('auth-types paste-callback start path', () => {
it('maps providers to CLIProxyAPI management auth-url routes', () => {
expect(getPasteCallbackStartPath('gemini')).toBe(
'/v0/management/gemini-cli-auth-url?is_webui=true'
);
expect(getPasteCallbackStartPath('codex')).toBe('/v0/management/codex-auth-url?is_webui=true');
expect(getPasteCallbackStartPath('agy')).toBe(
'/v0/management/antigravity-auth-url?is_webui=true'
);
expect(getPasteCallbackStartPath('claude')).toBe(
'/v0/management/anthropic-auth-url?is_webui=true'
);
expect(getPasteCallbackStartPath('ghcp')).toBe('/v0/management/github-auth-url?is_webui=true');
});
it('keeps Kiro on the legacy start route for paste-callback mode', () => {
expect(getPasteCallbackStartPath('kiro')).toBe('/oauth/kiro/start');
});
it('still exposes the generic management auth-url helper', () => {
expect(getManagementAuthUrlPath('kiro')).toBe('/v0/management/kiro-auth-url?is_webui=true');
});
it('uses CLIProxyAPI management oauth-callback route', () => {
expect(getManagementOAuthCallbackPath()).toBe('/v0/management/oauth-callback');
});
});
@@ -0,0 +1,99 @@
import { afterEach, describe, expect, it, mock } from 'bun:test';
describe('installCliproxyVersion', () => {
afterEach(() => {
mock.restore();
});
it('attempts to stop the proxy even when there is no tracked running session', async () => {
const calls = {
stopProxy: 0,
waitForPortFree: 0,
deleteBinary: 0,
ensureBinary: 0,
};
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
}));
mock.module('../../../src/cliproxy/config-generator', () => ({
getBinDir: () => '/tmp/ccs-bin',
CLIPROXY_DEFAULT_PORT: 8317,
}));
mock.module('../../../src/cliproxy/platform-detector', () => ({
DEFAULT_BACKEND: 'plus',
CLIPROXY_MAX_STABLE_VERSION: '9.9.999-0',
BACKEND_CONFIG: {
plus: {
fallbackVersion: '6.6.80',
repo: 'router-for-me/CLIProxyAPIPlus',
},
original: {
fallbackVersion: '0.0.0',
repo: 'router-for-me/CLIProxyAPI',
},
},
}));
mock.module('../../../src/cliproxy/services/proxy-lifecycle-service', () => ({
stopProxy: async () => {
calls.stopProxy += 1;
return { stopped: false, error: 'No active CLIProxy session found' };
},
}));
mock.module('../../../src/utils/port-utils', () => ({
waitForPortFree: async () => {
calls.waitForPortFree += 1;
return true;
},
}));
mock.module('../../../src/config/unified-config-loader', () => ({
loadOrCreateUnifiedConfig: () => ({
cliproxy: { backend: 'plus' },
}),
}));
mock.module('../../../src/cliproxy/binary', () => ({
checkForUpdates: async () => ({
hasUpdate: false,
currentVersion: '6.6.80',
latestVersion: '6.6.80',
fromCache: false,
checkedAt: Date.now(),
}),
deleteBinary: () => {
calls.deleteBinary += 1;
},
getBinaryPath: () => '/tmp/ccs-bin/plus/cliproxy',
isBinaryInstalled: () => false,
getBinaryInfo: async () => null,
getPinnedVersion: () => null,
savePinnedVersion: () => {},
clearPinnedVersion: () => {},
isVersionPinned: () => false,
getVersionPinPath: () => '/tmp/ccs-bin/plus/.version-pin',
readInstalledVersion: () => '6.6.80',
ensureBinary: async () => {
calls.ensureBinary += 1;
return '/tmp/ccs-bin/plus/cliproxy';
},
migrateVersionPin: () => {},
}));
const binaryManager = await import(
`../../../src/cliproxy/binary-manager?binary-manager-install=${Date.now()}`
);
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus');
expect(calls.stopProxy).toBe(1);
expect(calls.waitForPortFree).toBe(0);
expect(calls.deleteBinary).toBe(0);
expect(calls.ensureBinary).toBe(1);
});
});
+27
View File
@@ -106,6 +106,33 @@ describe('Model Catalog', () => {
});
});
describe('Claude models', () => {
it('contains Claude provider catalog', () => {
const { MODEL_CATALOG } = modelCatalog;
assert(MODEL_CATALOG.claude, 'Should have Claude provider');
assert.strictEqual(MODEL_CATALOG.claude.provider, 'claude');
assert.strictEqual(MODEL_CATALOG.claude.displayName, 'Claude (Anthropic)');
});
it('has correct default model', () => {
const { MODEL_CATALOG } = modelCatalog;
assert.strictEqual(MODEL_CATALOG.claude.defaultModel, 'claude-sonnet-4-6');
});
it('includes Claude Sonnet 4.6', () => {
const { MODEL_CATALOG } = modelCatalog;
const sonnet = MODEL_CATALOG.claude.models.find((m) => m.id === 'claude-sonnet-4-6');
assert(sonnet, 'Should include Claude Sonnet 4.6');
assert.strictEqual(sonnet.name, 'Claude Sonnet 4.6');
});
it('retains previous 4.5 snapshot models for explicit selection', () => {
const { MODEL_CATALOG } = modelCatalog;
const ids = MODEL_CATALOG.claude.models.map((m) => m.id);
assert(ids.includes('claude-opus-4-5-20251101'));
assert(ids.includes('claude-sonnet-4-5-20250929'));
});
});
describe('Gemini models', () => {
it('has correct default model', () => {
const { MODEL_CATALOG } = modelCatalog;
@@ -0,0 +1,104 @@
import { afterEach, describe, expect, it } from 'bun:test';
import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver';
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks';
const remoteTarget: ProxyTarget = {
host: 'proxy.example.com',
port: 8317,
protocol: 'https',
managementKey: 'test-mgmt-key',
isRemote: true,
};
afterEach(() => {
restoreFetch();
});
describe('requestPasteCallbackStart', () => {
it('uses management auth-url route for non-kiro providers', async () => {
mockFetch([
{
url: /\/v0\/management\/anthropic-auth-url\?is_webui=true$/,
response: { auth_url: 'https://auth.example.com/claude' },
},
]);
const { requestPasteCallbackStart } = await import(
`../../../src/cliproxy/auth/oauth-handler?request-claude-start=${Date.now()}`
);
const startData = await requestPasteCallbackStart('claude', remoteTarget);
expect(startData.auth_url).toBe('https://auth.example.com/claude');
const [request] = getCapturedFetchRequests();
expect(request.url).toBe(
'https://proxy.example.com:8317/v0/management/anthropic-auth-url?is_webui=true'
);
expect(request.method).toBe('GET');
expect(request.headers['Authorization']).toBe('Bearer test-mgmt-key');
expect(request.headers['Content-Type']).toBeUndefined();
});
it('keeps kiro on the legacy start route with POST', async () => {
mockFetch([
{
url: /\/oauth\/kiro\/start$/,
method: 'POST',
response: { auth_url: 'https://auth.example.com/kiro' },
},
]);
const { requestPasteCallbackStart } = await import(
`../../../src/cliproxy/auth/oauth-handler?request-kiro-start=${Date.now()}`
);
const startData = await requestPasteCallbackStart('kiro', remoteTarget);
expect(startData.auth_url).toBe('https://auth.example.com/kiro');
const [request] = getCapturedFetchRequests();
expect(request.url).toBe('https://proxy.example.com:8317/oauth/kiro/start');
expect(request.method).toBe('POST');
expect(request.headers['Authorization']).toBe('Bearer test-mgmt-key');
expect(request.headers['Content-Type']).toBe('application/json');
});
});
describe('resolvePasteCallbackAuthUrl', () => {
it('returns the immediate auth URL without polling', async () => {
const { resolvePasteCallbackAuthUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?resolve-immediate-auth-url=${Date.now()}`
);
const authUrl = await resolvePasteCallbackAuthUrl(
remoteTarget,
{ auth_url: 'https://auth.example.com/direct' },
50,
0
);
expect(authUrl).toBe('https://auth.example.com/direct');
expect(getCapturedFetchRequests()).toHaveLength(0);
});
it('polls management status when the start response only returns state', async () => {
mockFetch([
{
url: /\/v0\/management\/get-auth-status\?state=state-123$/,
response: { status: 'auth_url', auth_url: 'https://auth.example.com/polled' },
},
]);
const { resolvePasteCallbackAuthUrl } = await import(
`../../../src/cliproxy/auth/oauth-handler?resolve-polled-auth-url=${Date.now()}`
);
const authUrl = await resolvePasteCallbackAuthUrl(remoteTarget, { state: 'state-123' }, 50, 0);
expect(authUrl).toBe('https://auth.example.com/polled');
const [request] = getCapturedFetchRequests();
expect(request.url).toBe(
'https://proxy.example.com:8317/v0/management/get-auth-status?state=state-123'
);
expect(request.method).toBe('GET');
expect(request.headers['Authorization']).toBe('Bearer test-mgmt-key');
});
});
@@ -17,6 +17,7 @@ import {
getQuotaCacheStats,
QUOTA_CACHE_TTL_MS,
} from '../../../src/cliproxy/quota-response-cache';
import { shouldCacheQuotaResult } from '../../../src/web-server/routes/cliproxy-stats-routes';
import type { GeminiCliQuotaResult, CodexQuotaResult } from '../../../src/cliproxy/quota-types';
describe('Quota Caching Integration', () => {
@@ -308,6 +309,60 @@ describe('Quota Caching Integration', () => {
expect(cached?.error).toBe('API error: 503');
});
it('should cache stable auth and workspace failures', () => {
expect(
shouldCacheQuotaResult({
success: false,
needsReauth: true,
error: 'Token expired',
})
).toBe(true);
expect(
shouldCacheQuotaResult({
success: false,
httpStatus: 402,
error: 'Workspace deactivated (HTTP 402)',
})
).toBe(true);
});
it('should skip transient failures marked retryable or temporary by status', () => {
expect(
shouldCacheQuotaResult({
success: false,
retryable: true,
error: 'Rate limited - try again later',
})
).toBe(false);
expect(
shouldCacheQuotaResult({
success: false,
httpStatus: 429,
error: 'Rate limited - try again later',
})
).toBe(false);
expect(
shouldCacheQuotaResult({
success: false,
httpStatus: 503,
error: 'Codex quota service unavailable (HTTP 503)',
})
).toBe(false);
});
it('should respect explicit non-retryable failures even without message pattern matches', () => {
expect(
shouldCacheQuotaResult({
success: false,
retryable: false,
error: 'Unknown upstream error',
})
).toBe(true);
});
});
describe('high-volume scenarios', () => {
@@ -360,6 +360,136 @@ describe('Claude Quota Fetcher', () => {
expect(result.error).toContain('Authentication');
});
it('treats OAuth-unsupported 401 as policy-limits unavailable', async () => {
createClaudeAccount(
'claude-oauth-unsupported@example.com',
{
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
},
'claude'
);
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
type: 'error',
error: {
type: 'authentication_error',
message: 'OAuth authentication is currently not supported.',
},
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
)
) as typeof fetch;
const result = await fetchClaudeQuota('claude-oauth-unsupported@example.com');
expect(result.success).toBe(true);
expect(result.needsReauth).toBeUndefined();
expect(result.windows).toHaveLength(0);
expect(result.coreUsage?.fiveHour).toBeNull();
expect(result.coreUsage?.weekly).toBeNull();
});
it('treats root-level OAuth-unsupported 401 message as policy-limits unavailable', async () => {
createClaudeAccount('claude-oauth-root-message@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
message: 'OAuth authentication is currently not supported.',
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
)
) as typeof fetch;
const result = await fetchClaudeQuota('claude-oauth-root-message@example.com');
expect(result.success).toBe(true);
expect(result.needsReauth).toBeUndefined();
expect(result.windows).toHaveLength(0);
expect(result.coreUsage?.fiveHour).toBeNull();
expect(result.coreUsage?.weekly).toBeNull();
});
it('treats plain-text OAuth-unsupported 401 as policy-limits unavailable', async () => {
createClaudeAccount('claude-oauth-plaintext@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
global.fetch = mock(() =>
Promise.resolve(
new Response('OAuth authentication is currently not supported.', { status: 401 })
)
) as typeof fetch;
const result = await fetchClaudeQuota('claude-oauth-plaintext@example.com');
expect(result.success).toBe(true);
expect(result.needsReauth).toBeUndefined();
expect(result.windows).toHaveLength(0);
expect(result.coreUsage?.fiveHour).toBeNull();
expect(result.coreUsage?.weekly).toBeNull();
});
it('keeps non-matching 401 payloads in the reauth path', async () => {
createClaudeAccount('claude-auth-other-401@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
error: {
type: 'authentication_error',
message: 'Token revoked.',
},
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
)
) as typeof fetch;
const result = await fetchClaudeQuota('claude-auth-other-401@example.com');
expect(result.success).toBe(false);
expect(result.needsReauth).toBe(true);
expect(result.error).toContain('Authentication');
});
it('treats 404 policy limits responses as unavailable but successful', async () => {
createClaudeAccount('claude-policy-limits-404@example.com', {
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
});
global.fetch = mock(() => Promise.resolve(new Response('', { status: 404 }))) as typeof fetch;
const result = await fetchClaudeQuota('claude-policy-limits-404@example.com');
expect(result.success).toBe(true);
expect(result.needsReauth).toBeUndefined();
expect(result.windows).toHaveLength(0);
expect(result.coreUsage?.fiveHour).toBeNull();
expect(result.coreUsage?.weekly).toBeNull();
});
it('fails fast when auth file has no token', async () => {
createClaudeAccount('claude-missing@example.com', {
access_token: ' ',
+216 -1
View File
@@ -4,13 +4,48 @@
* Tests for Codex quota window parsing and transformation logic
*/
import { describe, it, expect } from 'bun:test';
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
buildCodexQuotaWindows,
buildCodexCoreUsageSummary,
fetchCodexQuota,
getUnknownCodexWindowLabels,
} from '../../../src/cliproxy/quota-fetcher-codex';
let tmpDir: string;
let originalCcsHome: string | undefined;
let originalFetch: typeof fetch;
function createCodexAccount(
accountId: string,
tokenPayload: Record<string, unknown>,
tokenFile = `codex-${accountId.replace(/[@.]/g, '_')}.json`
): void {
const authDir = path.join(tmpDir, '.ccs', 'cliproxy', 'auth');
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(path.join(authDir, tokenFile), JSON.stringify(tokenPayload));
}
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-quota-test-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpDir;
originalFetch = global.fetch;
});
afterEach(() => {
global.fetch = originalFetch;
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('Codex Quota Fetcher', () => {
describe('buildCodexQuotaWindows', () => {
it('should parse snake_case API response', () => {
@@ -307,4 +342,184 @@ describe('Codex Quota Fetcher', () => {
expect(labels).toEqual([]);
});
});
describe('fetchCodexQuota failure mapping', () => {
function createValidCodexAccount(email: string, accountId = `workspace-${email}`): void {
createCodexAccount(email, {
access_token: 'test-token',
account_id: accountId,
expired: '2099-01-01T00:00:00.000Z',
email,
type: 'codex',
});
}
it('maps deactivated workspace 402 responses to structured metadata', async () => {
createValidCodexAccount('workspace@example.com', 'workspace-123');
global.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ detail: { code: 'deactivated_workspace' } }), {
status: 402,
headers: { 'Content-Type': 'application/json' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('workspace@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(402);
expect(result.errorCode).toBe('deactivated_workspace');
expect(result.error).toContain('Workspace deactivated');
expect(result.actionHint).toContain('active ChatGPT workspace');
expect(result.retryable).toBe(false);
});
it('maps 401 responses to reauth-required metadata', async () => {
createValidCodexAccount('reauth@example.com', 'workspace-reauth');
global.fetch = mock(() => Promise.resolve(new Response('', { status: 401 }))) as typeof fetch;
const result = await fetchCodexQuota('reauth@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(401);
expect(result.errorCode).toBe('reauth_required');
expect(result.needsReauth).toBe(true);
expect(result.actionHint).toContain('ccs cliproxy auth codex');
});
it('maps 403 responses to forbidden metadata', async () => {
createValidCodexAccount('forbidden@example.com', 'workspace-forbidden');
global.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ detail: { code: 'quota_api_forbidden' } }), {
status: 403,
headers: { 'Content-Type': 'application/json' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('forbidden@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(403);
expect(result.errorCode).toBe('quota_api_forbidden');
expect(result.isForbidden).toBe(true);
expect(result.retryable).toBe(false);
});
it('maps 429 responses to retryable rate-limit metadata', async () => {
createValidCodexAccount('rate-limit@example.com', 'workspace-rate-limit');
global.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ detail: { code: 'rate_limited' } }), {
status: 429,
headers: { 'Content-Type': 'application/json' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('rate-limit@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(429);
expect(result.errorCode).toBe('rate_limited');
expect(result.retryable).toBe(true);
expect(result.actionHint).toContain('Retry');
});
it('maps 5xx responses to retryable provider-unavailable metadata', async () => {
createValidCodexAccount('outage@example.com', 'workspace-outage');
global.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify({ detail: { code: 'upstream_failure' } }), {
status: 503,
headers: { 'Content-Type': 'application/json' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('outage@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(503);
expect(result.errorCode).toBe('upstream_failure');
expect(result.retryable).toBe(true);
expect(result.error).toContain('service unavailable');
});
it('maps unknown upstream statuses to a non-retryable structured error', async () => {
createValidCodexAccount('teapot@example.com', 'workspace-teapot');
global.fetch = mock(() =>
Promise.resolve(
new Response('{"message":"Strange upstream response"}', {
status: 418,
headers: { 'Content-Type': 'application/json' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('teapot@example.com');
expect(result.success).toBe(false);
expect(result.httpStatus).toBe(418);
expect(result.errorCode).toBe('unknown_upstream_error');
expect(result.retryable).toBe(false);
expect(result.error).toBe('Strange upstream response');
});
it('sanitizes and truncates raw upstream error detail before returning it', async () => {
createValidCodexAccount('sanitized@example.com', 'workspace-sanitized');
const leakedToken = 'secret-token-value-123';
const oversizedMessage = 'x'.repeat(400);
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
message: 'Upstream failure',
access_token: leakedToken,
extra: oversizedMessage,
}),
{
status: 418,
headers: { 'Content-Type': 'application/json' },
}
)
)
) as typeof fetch;
const result = await fetchCodexQuota('sanitized@example.com');
expect(result.success).toBe(false);
expect(result.errorDetail).toBeDefined();
expect(result.errorDetail).not.toContain(leakedToken);
expect(result.errorDetail).toContain('[redacted]');
expect(result.errorDetail?.endsWith('...[truncated]')).toBe(true);
});
it('omits raw HTML upstream bodies from the returned error detail', async () => {
createValidCodexAccount('html@example.com', 'workspace-html');
global.fetch = mock(() =>
Promise.resolve(
new Response('<html><body>bad gateway</body></html>', {
status: 503,
headers: { 'Content-Type': 'text/html' },
})
)
) as typeof fetch;
const result = await fetchCodexQuota('html@example.com');
expect(result.success).toBe(false);
expect(result.errorDetail).toBe('[HTML error response omitted]');
});
});
});
@@ -4,13 +4,75 @@
* Tests for Gemini CLI bucket parsing and transformation logic
*/
import { describe, it, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import {
buildGeminiCliBuckets,
resolveGeminiCliProjectId,
} from '../../../src/cliproxy/quota-fetcher-gemini-cli';
import { refreshGeminiToken } from '../../../src/cliproxy/auth/gemini-token-refresh';
import { getProviderAuthDir } from '../../../src/cliproxy/config-generator';
import { getCapturedFetchRequests, mockFetch, restoreFetch } from '../../mocks';
describe('Gemini CLI Quota Fetcher', () => {
let tempHome: string;
let originalHome: string | undefined;
let originalCcsHome: string | undefined;
let originalGeminiClientId: string | undefined;
let originalGeminiClientSecret: string | undefined;
function writeGeminiToken(token: Record<string, unknown>): string {
const authDir = getProviderAuthDir('gemini');
fs.mkdirSync(authDir, { recursive: true });
const tokenPath = path.join(authDir, 'gemini-test.json');
fs.writeFileSync(tokenPath, JSON.stringify(token, null, 2));
return tokenPath;
}
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-gemini-refresh-'));
originalHome = process.env.HOME;
originalCcsHome = process.env.CCS_HOME;
originalGeminiClientId = process.env.CCS_GEMINI_OAUTH_CLIENT_ID;
originalGeminiClientSecret = process.env.CCS_GEMINI_OAUTH_CLIENT_SECRET;
process.env.HOME = tempHome;
process.env.CCS_HOME = tempHome;
delete process.env.CCS_GEMINI_OAUTH_CLIENT_ID;
delete process.env.CCS_GEMINI_OAUTH_CLIENT_SECRET;
});
afterEach(() => {
restoreFetch();
fs.rmSync(tempHome, { recursive: true, force: true });
if (originalHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = originalHome;
}
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (originalGeminiClientId === undefined) {
delete process.env.CCS_GEMINI_OAUTH_CLIENT_ID;
} else {
process.env.CCS_GEMINI_OAUTH_CLIENT_ID = originalGeminiClientId;
}
if (originalGeminiClientSecret === undefined) {
delete process.env.CCS_GEMINI_OAUTH_CLIENT_SECRET;
} else {
process.env.CCS_GEMINI_OAUTH_CLIENT_SECRET = originalGeminiClientSecret;
}
});
describe('resolveGeminiCliProjectId', () => {
it('should extract project ID from account field', () => {
const account = 'user@example.com (cloudaicompanion-abc-123)';
@@ -191,4 +253,85 @@ describe('Gemini CLI Quota Fetcher', () => {
expect(flashBucket!.modelIds).toContain('gemini-2.5-flash');
});
});
describe('refreshGeminiToken', () => {
it('uses OAuth client metadata stored in the token file', async () => {
writeGeminiToken({
type: 'gemini',
email: 'file@example.com',
token: {
access_token: 'old-token',
refresh_token: 'refresh-from-file',
expiry: Date.now() - 1000,
client_id: 'file-client-id',
client_secret: 'file-client-secret',
token_uri: 'https://oauth2.googleapis.com/token',
},
});
mockFetch([
{
url: 'https://oauth2.googleapis.com/token',
method: 'POST',
response: { access_token: 'fresh-token', expires_in: 1800 },
},
]);
const result = await refreshGeminiToken();
expect(result.success).toBe(true);
const [request] = getCapturedFetchRequests();
expect(request.body).toContain('client_id=file-client-id');
expect(request.body).toContain('client_secret=file-client-secret');
expect(request.body).toContain('refresh_token=refresh-from-file');
});
it('falls back to CCS_GEMINI_OAUTH_CLIENT_* env vars when token metadata is missing', async () => {
process.env.CCS_GEMINI_OAUTH_CLIENT_ID = 'env-client-id';
process.env.CCS_GEMINI_OAUTH_CLIENT_SECRET = 'env-client-secret';
writeGeminiToken({
type: 'gemini',
email: 'env@example.com',
token: {
access_token: 'old-token',
refresh_token: 'refresh-from-file',
expiry: Date.now() - 1000,
},
});
mockFetch([
{
url: 'https://oauth2.googleapis.com/token',
method: 'POST',
response: { access_token: 'fresh-token', expires_in: 1800 },
},
]);
const result = await refreshGeminiToken();
expect(result.success).toBe(true);
const [request] = getCapturedFetchRequests();
expect(request.body).toContain('client_id=env-client-id');
expect(request.body).toContain('client_secret=env-client-secret');
});
it('returns a clear error when no refresh client credentials are available', async () => {
writeGeminiToken({
type: 'gemini',
email: 'missing@example.com',
token: {
access_token: 'old-token',
refresh_token: 'refresh-from-file',
expiry: Date.now() - 1000,
},
});
const result = await refreshGeminiToken();
expect(result.success).toBe(false);
expect(result.error).toContain('CCS_GEMINI_OAUTH_CLIENT_ID');
expect(result.error).toContain('CCS_GEMINI_OAUTH_CLIENT_SECRET');
});
});
});
@@ -326,6 +326,227 @@ describe('ToolSanitizationProxy Integration', () => {
}
});
it('strips Gemini-unsupported top-level tool fields while keeping schema sanitization', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
await fetch(`http://127.0.0.1:${port}/api/provider/gemini/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gemini-2.5-pro',
tools: [
{
name: 'tool__with__duplicate__duplicate',
description: 'Test description',
strict: true,
input_examples: [{ command: 'ls -la' }],
type: 'custom',
cache_control: { type: 'ephemeral' },
defer_loading: true,
input_schema: {
type: 'object',
properties: {
command: {
type: 'string',
examples: ['ls -la'],
},
},
},
},
],
}),
});
const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array<
Record<string, unknown>
>;
expect(sentTools[0].name).toBe('tool__with__duplicate');
expect(sentTools[0].description).toBe('Test description');
expect(sentTools[0].strict).toBeUndefined();
expect(sentTools[0].input_examples).toBeUndefined();
expect(sentTools[0].type).toBeUndefined();
expect(sentTools[0].cache_control).toBeUndefined();
expect(sentTools[0].defer_loading).toBeUndefined();
expect(sentTools[0].input_schema).toEqual({
type: 'object',
properties: {
command: {
type: 'string',
},
},
});
} finally {
proxy.stop();
}
});
it('strips only Codex-unsupported top-level tool fields before forwarding', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
await fetch(`http://127.0.0.1:${port}/api/provider/codex/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.3-codex',
tools: [
{
name: 'codex_tool',
description: 'Codex test',
cache_control: { type: 'ephemeral' },
defer_loading: true,
input_schema: {
type: 'object',
properties: {
prompt: {
type: 'string',
examples: ['fix the failing test'],
},
},
},
},
],
}),
});
const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array<
Record<string, unknown>
>;
expect(sentTools[0].name).toBe('codex_tool');
expect(sentTools[0].description).toBe('Codex test');
expect(sentTools[0].cache_control).toBeUndefined();
expect(sentTools[0].defer_loading).toBe(true);
expect(sentTools[0].input_schema).toEqual({
type: 'object',
properties: {
prompt: {
type: 'string',
},
},
});
} finally {
proxy.stop();
}
});
for (const model of [
'gpt-5.3-codex-xhigh',
'gpt-5.1-codex-mini',
'gpt-5.1-codex',
'gpt-5-codex',
]) {
it(`strips only Codex-unsupported top-level tool fields on root model-routed request (${model})`, async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
await fetch(`http://127.0.0.1:${port}/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
tools: [
{
name: 'root_codex_tool',
description: 'Root-routed Codex test',
cache_control: { type: 'ephemeral' },
defer_loading: true,
input_schema: {
type: 'object',
properties: {
prompt: {
type: 'string',
examples: ['fix the failing test'],
},
},
},
},
],
}),
});
const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array<
Record<string, unknown>
>;
expect(sentTools[0].name).toBe('root_codex_tool');
expect(sentTools[0].description).toBe('Root-routed Codex test');
expect(sentTools[0].cache_control).toBeUndefined();
expect(sentTools[0].defer_loading).toBe(true);
expect(sentTools[0].input_schema).toEqual({
type: 'object',
properties: {
prompt: {
type: 'string',
},
},
});
} finally {
proxy.stop();
}
});
}
it('preserves top-level tool fields for non-target root routes', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
});
const port = await proxy.start();
try {
await fetch(`http://127.0.0.1:${port}/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'claude-sonnet-4-6',
tools: [
{
name: 'claude_tool__duplicate__duplicate',
description: 'Anthropic passthrough test',
cache_control: { type: 'ephemeral' },
defer_loading: true,
input_schema: {
type: 'object',
properties: {
prompt: {
type: 'string',
examples: ['keep these top-level fields'],
},
},
},
},
],
}),
});
const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array<
Record<string, unknown>
>;
expect(sentTools[0].name).toBe('claude_tool__duplicate');
expect(sentTools[0].description).toBe('Anthropic passthrough test');
expect(sentTools[0].cache_control).toEqual({ type: 'ephemeral' });
expect(sentTools[0].defer_loading).toBe(true);
expect(sentTools[0].input_schema).toEqual({
type: 'object',
properties: {
prompt: {
type: 'string',
},
},
});
} finally {
proxy.stop();
}
});
it('preserves other tool properties during sanitization', async () => {
const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
@@ -341,7 +562,10 @@ describe('ToolSanitizationProxy Integration', () => {
{
name: 'foo__bar__bar',
description: 'Test description',
input_schema: { type: 'object', properties: { x: { type: 'string' } } },
input_schema: {
type: 'object',
properties: { x: { type: 'string', examples: ['demo'] } },
},
},
],
}),
@@ -112,7 +112,7 @@ cliproxy:
expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.1-codex-mini');
expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.1-codex-mini');
expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.1-codex-mini');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.1-codex-mini');
expect(settings.env.CUSTOM_FLAG).toBe('keep-me');
expect(settings.hooks.PreToolUse.length).toBe(1);
@@ -134,7 +134,7 @@ cliproxy:
expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.1-codex-mini');
const modelOnly = updateVariant('demo', { model: 'gpt-5.3-codex' });
expect(modelOnly.success).toBe(true);
@@ -145,6 +145,6 @@ cliproxy:
expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
expect(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.1-codex-mini');
});
});
+84
View File
@@ -0,0 +1,84 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { handleSyncCommand } from '../../../src/commands/sync-command';
import { ClaudeDirInstaller } from '../../../src/utils/claude-dir-installer';
import { ClaudeSymlinkManager } from '../../../src/utils/claude-symlink-manager';
import SharedManager from '../../../src/management/shared-manager';
import { InstanceManager } from '../../../src/management/instance-manager';
import ProfileRegistry from '../../../src/auth/profile-registry';
import type { ProfileMetadata } from '../../../src/types';
function profile(metadata: Partial<ProfileMetadata> = {}): ProfileMetadata {
return {
type: 'account',
created: '2026-03-05T00:00:00.000Z',
last_used: null,
...metadata,
};
}
describe('sync command MCP sync behavior', () => {
let originalProcessExit: typeof process.exit;
beforeEach(() => {
originalProcessExit = process.exit;
process.exit = ((code?: number) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as typeof process.exit;
});
afterEach(() => {
process.exit = originalProcessExit;
mock.restore();
});
it('syncs MCP servers only to non-bare profiles', async () => {
spyOn(ClaudeDirInstaller.prototype, 'install').mockReturnValue(true);
spyOn(ClaudeDirInstaller.prototype, 'cleanupDeprecated').mockReturnValue({
success: true,
cleanedFiles: [],
});
spyOn(ClaudeSymlinkManager.prototype, 'install').mockImplementation(() => {});
spyOn(SharedManager.prototype, 'ensureSharedDirectories').mockImplementation(() => {});
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
work: profile(),
sandbox: profile({ bare: true }),
personal: profile(),
});
spyOn(InstanceManager.prototype, 'hasInstance').mockReturnValue(true);
const getInstancePathSpy = spyOn(InstanceManager.prototype, 'getInstancePath').mockImplementation(
(name: string) => `/tmp/${name}`
);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => true
);
await expect(handleSyncCommand()).rejects.toThrow('process.exit(0)');
expect(getInstancePathSpy.mock.calls.map((call) => call[0])).toEqual(['work', 'personal']);
expect(syncMcpSpy.mock.calls.map((call) => call[0])).toEqual(['/tmp/work', '/tmp/personal']);
});
it('skips MCP sync when all profiles are bare', async () => {
spyOn(ClaudeDirInstaller.prototype, 'install').mockReturnValue(true);
spyOn(ClaudeDirInstaller.prototype, 'cleanupDeprecated').mockReturnValue({
success: true,
cleanedFiles: [],
});
spyOn(ClaudeSymlinkManager.prototype, 'install').mockImplementation(() => {});
spyOn(SharedManager.prototype, 'ensureSharedDirectories').mockImplementation(() => {});
spyOn(ProfileRegistry.prototype, 'getAllProfilesMerged').mockReturnValue({
sandbox: profile({ bare: true }),
experiment: profile({ bare: true }),
});
spyOn(InstanceManager.prototype, 'hasInstance').mockReturnValue(true);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => true
);
await expect(handleSyncCommand()).rejects.toThrow('process.exit(0)');
expect(syncMcpSpy).not.toHaveBeenCalled();
});
});
+96
View File
@@ -0,0 +1,96 @@
import { describe, expect, it } from 'bun:test';
import * as http from 'http';
import { DEFAULT_COPILOT_MODELS, fetchModelsFromDaemon } from '../../../src/copilot/copilot-models';
describe('fetchModelsFromDaemon', () => {
it('falls back to defaults when daemon is unreachable', async () => {
const models = await fetchModelsFromDaemon(9999);
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
});
it('falls back to defaults when daemon returns invalid JSON', async () => {
const server = http.createServer((_req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{not-valid-json');
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
try {
const models = await fetchModelsFromDaemon(address.port);
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it('parses live limits from daemon metadata and preserves known model metadata', async () => {
const server = http.createServer((_req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
data: [
{
id: 'claude-sonnet-4.5',
name: 'Claude Sonnet 4.5',
capabilities: {
limits: {
max_context_window_tokens: 128000,
max_prompt_tokens: 128000,
max_output_tokens: 64000,
},
},
},
{
id: 'claude-sonnet-4.6',
name: 'Claude Sonnet 4.6',
capabilities: {
limits: {
max_context_window_tokens: 128000,
max_prompt_tokens: 128000,
max_output_tokens: 64000,
},
},
},
{
id: '',
name: 'invalid-entry',
},
],
})
);
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
try {
const models = await fetchModelsFromDaemon(address.port);
expect(models).toHaveLength(2);
const knownModel = models.find((model) => model.id === 'claude-sonnet-4.5');
expect(knownModel?.provider).toBe('anthropic');
expect(knownModel?.minPlan).toBe('pro');
expect(knownModel?.multiplier).toBe(1);
expect(knownModel?.limits).toEqual({
maxContextWindowTokens: 128000,
maxPromptTokens: 128000,
maxOutputTokens: 64000,
});
const liveOnlyModel = models.find((model) => model.id === 'claude-sonnet-4.6');
expect(liveOnlyModel?.name).toBe('Claude Sonnet 4.6');
expect(liveOnlyModel?.provider).toBe('anthropic');
expect(liveOnlyModel?.limits?.maxPromptTokens).toBe(128000);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
});
@@ -0,0 +1,114 @@
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { InstanceManager } from '../../src/management/instance-manager';
import SharedManager from '../../src/management/shared-manager';
describe('InstanceManager MCP sync', () => {
let tempRoot = '';
let originalCcsHome: string | undefined;
let originalCcsDir: string | undefined;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-instance-mcp-test-'));
originalCcsHome = process.env.CCS_HOME;
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_HOME = tempRoot;
delete process.env.CCS_DIR;
});
afterEach(() => {
mock.restore();
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
if (originalCcsDir !== undefined) process.env.CCS_DIR = originalCcsDir;
else delete process.env.CCS_DIR;
if (tempRoot && fs.existsSync(tempRoot)) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
it('merges global MCP servers and preserves instance-specific overrides', () => {
fs.writeFileSync(
path.join(tempRoot, '.claude.json'),
JSON.stringify(
{
mcpServers: {
globalOnly: { command: 'global-cmd' },
shared: { command: 'global-shared' },
},
},
null,
2
),
'utf8'
);
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('work');
fs.mkdirSync(instancePath, { recursive: true });
fs.writeFileSync(
path.join(instancePath, '.claude.json'),
JSON.stringify(
{
mcpServers: {
shared: { command: 'instance-shared' },
instanceOnly: { command: 'instance-only' },
},
otherKey: 'keep-me',
},
null,
2
),
'utf8'
);
const synced = manager.syncMcpServers(instancePath);
expect(synced).toBe(true);
const instanceContent = JSON.parse(fs.readFileSync(path.join(instancePath, '.claude.json'), 'utf8'));
expect(instanceContent.otherKey).toBe('keep-me');
expect(instanceContent.mcpServers).toEqual({
globalOnly: { command: 'global-cmd' },
shared: { command: 'instance-shared' },
instanceOnly: { command: 'instance-only' },
});
});
it('logs warning when global MCP sync fails', () => {
fs.writeFileSync(path.join(tempRoot, '.claude.json'), '{invalid-json', 'utf8');
const warnSpy = spyOn(console, 'warn').mockImplementation(() => {});
const manager = new InstanceManager();
const instancePath = manager.getInstancePath('work');
fs.mkdirSync(instancePath, { recursive: true });
const synced = manager.syncMcpServers(instancePath);
expect(synced).toBe(false);
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(String(warnSpy.mock.calls[0]?.[0] || '')).toContain('MCP sync skipped');
});
it('skips shared symlinks and MCP sync for bare instance creation', async () => {
const linkSharedSpy = spyOn(SharedManager.prototype, 'linkSharedDirectories').mockImplementation(
() => {}
);
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
() => false
);
const manager = new InstanceManager();
await manager.ensureInstance('sandbox', { mode: 'isolated' }, { bare: true });
expect(linkSharedSpy).not.toHaveBeenCalled();
expect(syncMcpSpy).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'bun:test';
import {
compareCliproxyVersions,
isCliproxyVersionExperimental,
isCliproxyVersionInRange,
} from '../../../ui/src/lib/cliproxy-version-risk';
describe('cliproxy-version-risk helpers', () => {
it('compares versions while ignoring release suffixes', () => {
expect(compareCliproxyVersions('6.6.88', '6.6.81-0')).toBe(1);
expect(compareCliproxyVersions('6.6.81-0', '6.6.81')).toBe(0);
expect(compareCliproxyVersions('6.6.80', '6.6.81')).toBe(-1);
});
it('detects experimental versions against max stable', () => {
expect(isCliproxyVersionExperimental('10.0.0', '9.9.999-0')).toBe(true);
expect(isCliproxyVersionExperimental('6.6.88', '9.9.999-0')).toBe(false);
});
it('detects versions inside the faulty range', () => {
expect(isCliproxyVersionInRange('6.6.81', '6.6.81-0', '6.6.88-0')).toBe(true);
expect(isCliproxyVersionInRange('6.6.88', '6.6.81-0', '6.6.88-0')).toBe(true);
expect(isCliproxyVersionInRange('6.6.89', '6.6.81-0', '6.6.88-0')).toBe(false);
});
});
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'bun:test';
import { ERROR_CODES, getErrorCategory, getErrorDocUrl } from '../../../src/utils/error-codes';
describe('error-codes', () => {
it('builds live docs URLs with lowercase anchors', () => {
expect(getErrorDocUrl(ERROR_CODES.PROFILE_NOT_FOUND)).toBe(
'https://docs.ccs.kaitran.ca/reference/error-codes#e104'
);
expect(getErrorDocUrl(ERROR_CODES.INTERNAL_ERROR)).toBe(
'https://docs.ccs.kaitran.ca/reference/error-codes#e900'
);
});
it('maps numeric ranges to human-readable categories', () => {
expect(getErrorCategory(ERROR_CODES.CONFIG_MISSING)).toBe('Configuration');
expect(getErrorCategory(ERROR_CODES.PROFILE_NOT_FOUND)).toBe('Profile Management');
expect(getErrorCategory(ERROR_CODES.CLAUDE_NOT_FOUND)).toBe('Claude CLI Detection');
expect(getErrorCategory(ERROR_CODES.API_AUTH_FAILED)).toBe('Network/API');
expect(getErrorCategory(ERROR_CODES.FS_CANNOT_READ_FILE)).toBe('File System');
expect(getErrorCategory(ERROR_CODES.INVALID_STATE)).toBe('Internal');
});
});
@@ -0,0 +1,125 @@
import { describe, expect, it } from 'bun:test';
import type { CLIProxyBackend } from '../../../src/cliproxy/types';
import {
installDashboardCliproxyVersion,
type DashboardCliproxyInstallResult,
} from '../../../src/web-server/services/cliproxy-dashboard-install-service';
function createDeps(
overrides: {
sessionRunning?: boolean;
remoteRunning?: boolean;
startResult?: { started: boolean; alreadyRunning: boolean; port: number; error?: string };
} = {}
) {
const calls = {
isCliproxyRunning: 0,
installCliproxyVersion: 0,
ensureCliproxyService: 0,
};
const deps = {
getProxyStatus: () => ({ running: overrides.sessionRunning ?? false }),
isCliproxyRunning: async () => {
calls.isCliproxyRunning += 1;
return overrides.remoteRunning ?? false;
},
installCliproxyVersion: async (
_version: string,
_verbose?: boolean,
_backend?: CLIProxyBackend
) => {
calls.installCliproxyVersion += 1;
},
ensureCliproxyService: async () => {
calls.ensureCliproxyService += 1;
return (
overrides.startResult ?? {
started: true,
alreadyRunning: false,
port: 8317,
}
);
},
};
return { deps, calls };
}
describe('installDashboardCliproxyVersion', () => {
it('restarts the proxy after install when it was already running', async () => {
const { deps, calls } = createDeps({ sessionRunning: true });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: true,
restarted: true,
port: 8317,
message: 'Successfully installed CLIProxy Plus v6.7.1 and restarted it on port 8317',
});
expect(calls.isCliproxyRunning).toBe(0);
expect(calls.installCliproxyVersion).toBe(1);
expect(calls.ensureCliproxyService).toBe(1);
});
it('keeps the proxy stopped after install when it was not running beforehand', async () => {
const { deps, calls } = createDeps({ sessionRunning: false, remoteRunning: false });
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: true,
restarted: false,
message: 'Successfully installed CLIProxy Plus v6.7.1',
});
expect(calls.isCliproxyRunning).toBe(1);
expect(calls.installCliproxyVersion).toBe(1);
expect(calls.ensureCliproxyService).toBe(0);
});
it('reports a restart failure after a successful install when the proxy had been running', async () => {
const { deps, calls } = createDeps({
sessionRunning: false,
remoteRunning: true,
startResult: {
started: false,
alreadyRunning: false,
port: 8317,
error: 'Port 8317 is blocked by another process',
},
});
const result = await installDashboardCliproxyVersion('6.7.1', 'original', deps);
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: false,
restarted: false,
error: 'Port 8317 is blocked by another process',
message: 'Installed CLIProxy v6.7.1, but failed to restart it',
});
expect(calls.isCliproxyRunning).toBe(1);
expect(calls.installCliproxyVersion).toBe(1);
expect(calls.ensureCliproxyService).toBe(1);
});
it('uses a fallback restart error when the start result omits one', async () => {
const { deps } = createDeps({
remoteRunning: true,
startResult: {
started: false,
alreadyRunning: false,
port: 8317,
},
});
const result = await installDashboardCliproxyVersion('6.7.1', 'plus', deps);
expect(result).toEqual<DashboardCliproxyInstallResult>({
success: false,
restarted: false,
error: 'Installed CLIProxy Plus v6.7.1, but restart failed',
message: 'Installed CLIProxy Plus v6.7.1, but failed to restart it',
});
});
});
@@ -0,0 +1,168 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
import express from 'express';
import type { Server } from 'http';
const installSpy = {
calls: 0,
};
mock.module('../../../src/config/unified-config-loader', () => ({
loadOrCreateUnifiedConfig: () => ({
cliproxy: { backend: 'plus' },
}),
}));
mock.module('../../../src/cliproxy/binary-manager', () => ({
checkCliproxyUpdate: async () => ({
hasUpdate: false,
currentVersion: '6.6.80',
latestVersion: '6.6.89',
fromCache: false,
checkedAt: Date.now(),
backend: 'plus',
backendLabel: 'CLIProxy Plus',
isStable: true,
maxStableVersion: '9.9.999-0',
}),
getInstalledCliproxyVersion: () => '6.6.80',
installCliproxyVersion: async () => {},
}));
mock.module('../../../src/cliproxy/binary/version-checker', () => ({
fetchAllVersions: async () => ({
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
latestStable: '6.6.89',
latest: '6.6.89',
fromCache: false,
checkedAt: Date.now(),
}),
isNewerVersion: (version: string, maxStable: string) => {
const normalize = (value: string) => value.replace(/-\d+$/, '').split('.').map(Number);
const versionParts = normalize(version);
const maxStableParts = normalize(maxStable);
for (let index = 0; index < 3; index += 1) {
const versionPart = versionParts[index] || 0;
const maxStablePart = maxStableParts[index] || 0;
if (versionPart > maxStablePart) return true;
if (versionPart < maxStablePart) return false;
}
return false;
},
isVersionFaulty: (version: string) =>
['6.6.81', '6.6.82', '6.6.83', '6.6.84', '6.6.85', '6.6.86', '6.6.87', '6.6.88'].includes(
version
),
}));
mock.module('../../../src/web-server/services/cliproxy-dashboard-install-service', () => ({
installDashboardCliproxyVersion: async () => {
installSpy.calls += 1;
return {
success: true,
restarted: true,
port: 8317,
message: 'installed',
};
},
}));
let cliproxyStatsRoutes: typeof import('../../../src/web-server/routes/cliproxy-stats-routes').default;
let server: Server;
let baseUrl = '';
beforeAll(async () => {
cliproxyStatsRoutes = (await import('../../../src/web-server/routes/cliproxy-stats-routes'))
.default;
const app = express();
app.use(express.json());
app.use('/api/cliproxy', cliproxyStatsRoutes);
await new Promise<void>((resolve, reject) => {
server = app.listen(0, '127.0.0.1');
const onError = (error: Error) => reject(error);
server.once('error', onError);
server.once('listening', () => {
server.off('error', onError);
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(() => {
installSpy.calls = 0;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
mock.restore();
});
describe('cliproxy-stats-routes install contract', () => {
it('returns faultyRange in the versions response', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/versions`);
expect(response.status).toBe(200);
const body = (await response.json()) as {
faultyRange: { min: string; max: string };
currentVersion: string;
};
expect(body.currentVersion).toBe('6.6.80');
expect(body.faultyRange).toEqual({ min: '6.6.81-0', max: '6.6.88-0' });
});
it('returns faulty confirmation metadata without calling the installer', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: '6.6.81' }),
});
expect(response.status).toBe(200);
const body = (await response.json()) as {
success: boolean;
requiresConfirmation: boolean;
isFaulty: boolean;
isExperimental: boolean;
message: string;
};
expect(body.success).toBe(false);
expect(body.requiresConfirmation).toBe(true);
expect(body.isFaulty).toBe(true);
expect(body.isExperimental).toBe(false);
expect(body.message).toContain('known bugs');
expect(installSpy.calls).toBe(0);
});
it('returns experimental confirmation metadata without calling the installer', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: '10.0.0' }),
});
expect(response.status).toBe(200);
const body = (await response.json()) as {
success: boolean;
requiresConfirmation: boolean;
isFaulty: boolean;
isExperimental: boolean;
message: string;
};
expect(body.success).toBe(false);
expect(body.requiresConfirmation).toBe(true);
expect(body.isFaulty).toBe(false);
expect(body.isExperimental).toBe(true);
expect(body.message).toContain('experimental');
expect(installSpy.calls).toBe(0);
});
});
@@ -0,0 +1,145 @@
import { afterAll, afterEach, 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';
import profileRoutes from '../../../src/web-server/routes/profile-routes';
describe('profile-routes lifecycle endpoints', () => {
let server: Server;
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
beforeAll(async () => {
const app = express();
app.use(express.json());
app.use('/api/profiles', profileRoutes);
await new Promise<void>((resolve, reject) => {
server = app.listen(0, '127.0.0.1');
const onError = (error: Error) => reject(error);
server.once('error', onError);
server.once('listening', () => {
server.off('error', onError);
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}`;
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-routes-lifecycle-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it('rejects unknown fields on profile create payload', async () => {
const response = await fetch(`${baseUrl}/api/profiles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'demo',
baseUrl: 'https://api.example.com',
apiKey: 'token',
unknownField: true,
}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('Unknown profile field(s)');
});
it('does not register all orphans when names=[] is explicitly passed', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
fs.writeFileSync(path.join(ccsDir, 'config.json'), JSON.stringify({ profiles: {} }, null, 2) + '\n');
fs.writeFileSync(
path.join(ccsDir, 'lonely.settings.json'),
JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'token' } }, null, 2) +
'\n'
);
const response = await fetch(`${baseUrl}/api/profiles/orphans/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names: [] }),
});
expect(response.status).toBe(200);
const body = (await response.json()) as { registered: string[]; skipped: Array<unknown> };
expect(body.registered).toEqual([]);
expect(body.skipped).toEqual([]);
});
it('rejects malformed names payload for orphan registration', async () => {
const response = await fetch(`${baseUrl}/api/profiles/orphans/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ names: 'lonely' }),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('names must be an array');
});
it('rejects import bundle with invalid profile target', async () => {
const response = await fetch(`${baseUrl}/api/profiles/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bundle: {
schemaVersion: 1,
exportedAt: new Date().toISOString(),
profile: { name: 'demo', target: 'invalid' },
settings: {
env: {
ANTHROPIC_BASE_URL: 'https://api.example.com',
ANTHROPIC_AUTH_TOKEN: 'token',
},
},
},
}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('Invalid bundle profile target');
});
it('validates source profile name on export endpoint', async () => {
const response = await fetch(`${baseUrl}/api/profiles/1invalid/export`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
expect(response.status).toBe(400);
const body = (await response.json()) as { error: string };
expect(body.error).toContain('API name must start with letter');
});
});