mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
fix(cliproxy): exclude colocated tests from tsconfig and eslint
- Add src/**/__tests__/** to tsconfig.json exclude list - Add ignores pattern for __tests__ in eslint.config.mjs - Fix ui/src/lib/api-client.ts import path for provider-entitlement-types - Remove stale build artifacts from ui/src/lib/ and tests/mocks/
This commit is contained in:
@@ -5,6 +5,7 @@ import prettier from 'eslint-config-prettier';
|
||||
export default [
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
ignores: ['src/**/__tests__/**'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
|
||||
@@ -16,7 +16,8 @@ function writeCliproxyConfig(homeDir: string, value: Record<string, unknown>): v
|
||||
|
||||
function readCliproxyConfig(homeDir: string): Record<string, any> {
|
||||
return (
|
||||
(yaml.load(fs.readFileSync(getCliproxyConfigPath(homeDir), 'utf8')) as Record<string, any>) || {}
|
||||
(yaml.load(fs.readFileSync(getCliproxyConfigPath(homeDir), 'utf8')) as Record<string, any>) ||
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +63,9 @@ describe('ai-provider service stable ids', () => {
|
||||
expect(family?.entries[1]?.id).toBeTruthy();
|
||||
expect(family?.entries[0]?.id).not.toBe(family?.entries[1]?.id);
|
||||
|
||||
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
||||
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
expect(persisted[0]?.id).toBe(family?.entries[0]?.id);
|
||||
expect(persisted[1]?.id).toBe(family?.entries[1]?.id);
|
||||
});
|
||||
@@ -82,7 +85,9 @@ describe('ai-provider service stable ids', () => {
|
||||
baseUrl: 'https://example.test/gemini',
|
||||
});
|
||||
|
||||
let persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
||||
let persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
expect(persisted[0]?.id).toBe('gemini-a');
|
||||
expect(persisted[0]?.['api-key']).toBe('gamma');
|
||||
expect(persisted[0]?.['base-url']).toBe('https://example.test/gemini');
|
||||
@@ -105,7 +110,9 @@ describe('ai-provider service stable ids', () => {
|
||||
apiKey: 'legacy-index-update',
|
||||
});
|
||||
|
||||
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
||||
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
expect(persisted[0]?.id).toBe('gemini-a');
|
||||
expect(persisted[0]?.['api-key']).toBe('legacy-index-update');
|
||||
});
|
||||
@@ -140,9 +147,9 @@ describe('ai-provider service stable ids', () => {
|
||||
>;
|
||||
expect(persisted[0]?.id).toBe(connectorId);
|
||||
expect(persisted[0]?.['base-url']).toBe('https://router.example/v1');
|
||||
expect((persisted[0]?.['api-key-entries'] as Array<Record<string, unknown>>)[0]?.['api-key']).toBe(
|
||||
'sk-openrouter'
|
||||
);
|
||||
expect(
|
||||
(persisted[0]?.['api-key-entries'] as Array<Record<string, unknown>>)[0]?.['api-key']
|
||||
).toBe('sk-openrouter');
|
||||
});
|
||||
|
||||
it('normalizes plain openai-compatible model rules without aliases', async () => {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { findModel, supportsThinking } from '../model-catalog';
|
||||
import {
|
||||
PROVIDER_TO_CHANNEL,
|
||||
SYNCABLE_PROVIDERS,
|
||||
mergeCatalog,
|
||||
} from '../services/catalog-cache';
|
||||
import { PROVIDER_TO_CHANNEL, SYNCABLE_PROVIDERS, mergeCatalog } from '../services/catalog-cache';
|
||||
|
||||
describe('model-catalog compatibility lookups', () => {
|
||||
it('finds agy Claude models using dotted major.minor IDs', () => {
|
||||
|
||||
@@ -118,10 +118,7 @@ describe('Model Config', () => {
|
||||
|
||||
assert(parsed.env, 'Should have env object');
|
||||
assert(parsed.env.ANTHROPIC_MODEL, 'Should have ANTHROPIC_MODEL');
|
||||
assert.strictEqual(
|
||||
parsed.env.ANTHROPIC_MODEL,
|
||||
'claude-opus-4-5-thinking'
|
||||
);
|
||||
assert.strictEqual(parsed.env.ANTHROPIC_MODEL, 'claude-opus-4-5-thinking');
|
||||
});
|
||||
|
||||
it('all env values should be strings (PowerShell safety)', () => {
|
||||
|
||||
@@ -167,9 +167,13 @@ describe('provider-capabilities', () => {
|
||||
expect(getKiroCLIAuthFlag('aws-authcode')).toBe('--kiro-aws-authcode');
|
||||
expect(getKiroCLIAuthFlag('google')).toBe('--kiro-google-login');
|
||||
expect(getKiroCLIAuthFlag('idc')).toBe('--kiro-idc-login');
|
||||
expect(getKiroCLIAuthArgs('idc', { idcStartUrl: 'https://d-123.awsapps.com/start' })).toEqual(
|
||||
['--kiro-idc-login', '--kiro-idc-start-url', 'https://d-123.awsapps.com/start', '--kiro-idc-flow', 'authcode']
|
||||
);
|
||||
expect(getKiroCLIAuthArgs('idc', { idcStartUrl: 'https://d-123.awsapps.com/start' })).toEqual([
|
||||
'--kiro-idc-login',
|
||||
'--kiro-idc-start-url',
|
||||
'https://d-123.awsapps.com/start',
|
||||
'--kiro-idc-flow',
|
||||
'authcode',
|
||||
]);
|
||||
|
||||
expect(getKiroCallbackPort('aws')).toBeNull();
|
||||
expect(getKiroCallbackPort('google')).toBe(9876);
|
||||
|
||||
@@ -11,7 +11,10 @@ const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Set test isolation environment before importing
|
||||
const testHome = path.join(os.tmpdir(), `ccs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
const testHome = path.join(
|
||||
os.tmpdir(),
|
||||
`ccs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
);
|
||||
const originalCcsHome = process.env.CCS_HOME;
|
||||
const originalCcsDir = process.env.CCS_DIR;
|
||||
process.env.CCS_HOME = testHome;
|
||||
|
||||
@@ -183,7 +183,11 @@ describe('Account Manager - discoverExistingAccounts', () => {
|
||||
|
||||
const accountIds = Object.keys(accounts.providers.kiro.accounts);
|
||||
// For kiro/ghcp without email, generates unique ID like "kiro-1"
|
||||
assert.strictEqual(accountIds[0], 'kiro-1', 'Should generate unique ID for kiro without email');
|
||||
assert.strictEqual(
|
||||
accountIds[0],
|
||||
'kiro-1',
|
||||
'Should generate unique ID for kiro without email'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles dots in email local part', () => {
|
||||
|
||||
@@ -21,7 +21,9 @@ async function loadAccountManager() {
|
||||
return import(`../account-manager?account-registry-integrity=${Date.now()}`);
|
||||
}
|
||||
|
||||
async function captureConsoleError<T>(fn: () => Promise<T> | T): Promise<{ result: T; messages: string[] }> {
|
||||
async function captureConsoleError<T>(
|
||||
fn: () => Promise<T> | T
|
||||
): Promise<{ result: T; messages: string[] }> {
|
||||
const originalError = console.error;
|
||||
const messages: string[] = [];
|
||||
|
||||
@@ -351,7 +353,9 @@ describe('account registry integrity', () => {
|
||||
fs.writeFileSync(registryPath, '{"providers":', 'utf8');
|
||||
|
||||
const { loadAccountsRegistry } = await loadRegistryModule();
|
||||
const { result: registry, messages } = await captureConsoleError(() => loadAccountsRegistry());
|
||||
const { result: registry, messages } = await captureConsoleError(() =>
|
||||
loadAccountsRegistry()
|
||||
);
|
||||
|
||||
expect(registry).toEqual({
|
||||
version: 1,
|
||||
|
||||
@@ -121,7 +121,8 @@ describe('registerAccount optional nickname flow', () => {
|
||||
const result = await withIsolatedHome(async (homeDir) => {
|
||||
writeTokenFile(homeDir, 'codex-04a0f049-kaidu.kd@gmail.com-team.json');
|
||||
writeTokenFile(homeDir, 'codex-kaidu.kd@gmail.com-free.json');
|
||||
const { registerAccount, getProviderAccounts, findAccountByQuery } = await loadAccountManager();
|
||||
const { registerAccount, getProviderAccounts, findAccountByQuery } =
|
||||
await loadAccountManager();
|
||||
const team = registerAccount(
|
||||
'codex',
|
||||
'codex-04a0f049-kaidu.kd@gmail.com-team.json',
|
||||
|
||||
@@ -342,16 +342,12 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await handleQuotaExhaustion('claude', 'exhausted@example.com', 10);
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedTo).toBe('fallback@example.com');
|
||||
expect(getDefaultAccount('claude')?.id).toBe('fallback@example.com');
|
||||
expect(getAccount('claude', 'exhausted@example.com')?.paused).not.toBe(true);
|
||||
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(
|
||||
false
|
||||
);
|
||||
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(false);
|
||||
expect(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
@@ -429,9 +425,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await handleQuotaExhaustion('codex', 'exhausted@example.com', 10);
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedTo).toBe('fallback@example.com');
|
||||
expect(getDefaultAccount('codex')?.id).toBe('fallback@example.com');
|
||||
@@ -569,9 +563,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
|
||||
const { preflightCheck } = await import('../../quota/quota-manager');
|
||||
const result = await preflightCheck('codex');
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedFrom).toBe('preflight-exhausted@example.com');
|
||||
expect(result.accountId).toBe('preflight-fallback@example.com');
|
||||
@@ -649,16 +641,12 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
|
||||
const { preflightCheck } = await import('../../quota/quota-manager');
|
||||
const result = await preflightCheck('codex');
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedFrom).toBe('preflight-unknown-exhausted@example.com');
|
||||
expect(result.accountId).toBe('preflight-unknown-fallback@example.com');
|
||||
expect(getDefaultAccount('codex')?.id).toBe('preflight-unknown-fallback@example.com');
|
||||
expect(getAccount('codex', 'preflight-unknown-exhausted@example.com')?.paused).not.toBe(
|
||||
true
|
||||
);
|
||||
expect(getAccount('codex', 'preflight-unknown-exhausted@example.com')?.paused).not.toBe(true);
|
||||
expect(
|
||||
fs.existsSync(
|
||||
path.join(tmpDir, '.ccs', 'cliproxy', 'auth', 'codex-preflight-unknown-exhausted.json')
|
||||
@@ -675,9 +663,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
)
|
||||
)
|
||||
).toBe(false);
|
||||
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(
|
||||
false
|
||||
);
|
||||
expect(fs.existsSync(path.join(tmpDir, '.ccs', 'cliproxy', 'quota-paused.json'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should self-pause exhausted Gemini accounts when a healthy fallback exists', async () => {
|
||||
@@ -736,9 +722,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await handleQuotaExhaustion('gemini', 'gemini-exhausted@example.com', 10);
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedTo).toBe('gemini-fallback@example.com');
|
||||
expect(getDefaultAccount('gemini')?.id).toBe('gemini-fallback@example.com');
|
||||
@@ -804,9 +788,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await handleQuotaExhaustion('ghcp', 'ghcp-exhausted', 10);
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedTo).toBe('ghcp-fallback');
|
||||
expect(getDefaultAccount('ghcp')?.id).toBe('ghcp-fallback');
|
||||
@@ -868,9 +850,7 @@ describe('Quota Exhaustion Handlers', () => {
|
||||
}) as typeof fetch;
|
||||
|
||||
const result = await handleQuotaExhaustion('ghcp', 'ghcp-partial-exhausted', 10);
|
||||
const { getAccount, getDefaultAccount } = await import(
|
||||
'../account-manager'
|
||||
);
|
||||
const { getAccount, getDefaultAccount } = await import('../account-manager');
|
||||
|
||||
expect(result.switchedTo).toBe('ghcp-partial-fallback');
|
||||
expect(getDefaultAccount('ghcp')?.id).toBe('ghcp-partial-fallback');
|
||||
|
||||
@@ -153,7 +153,10 @@ describe('Codex Reasoning Proxy', () => {
|
||||
});
|
||||
|
||||
it('preserves query strings after stripping', () => {
|
||||
const result = stripPathPrefix('/api/provider/codex/v1/messages?stream=true', '/api/provider/codex');
|
||||
const result = stripPathPrefix(
|
||||
'/api/provider/codex/v1/messages?stream=true',
|
||||
'/api/provider/codex'
|
||||
);
|
||||
assert.strictEqual(result, '/v1/messages?stream=true');
|
||||
});
|
||||
|
||||
|
||||
@@ -56,8 +56,15 @@ describe('openai-compat manager', () => {
|
||||
});
|
||||
|
||||
const afterWrite = fs.readFileSync(configPath, 'utf8');
|
||||
assert.strictEqual(afterWrite.split('\n')[0], initialHeader, 'Should preserve the generated header');
|
||||
assert(afterWrite.includes('openai-compatibility:'), 'Should write the openai-compatibility section');
|
||||
assert.strictEqual(
|
||||
afterWrite.split('\n')[0],
|
||||
initialHeader,
|
||||
'Should preserve the generated header'
|
||||
);
|
||||
assert(
|
||||
afterWrite.includes('openai-compatibility:'),
|
||||
'Should write the openai-compatibility section'
|
||||
);
|
||||
assert.strictEqual(
|
||||
configGenerator.configNeedsRegeneration(),
|
||||
false,
|
||||
@@ -67,7 +74,10 @@ describe('openai-compat manager', () => {
|
||||
configGenerator.regenerateConfig();
|
||||
|
||||
const afterRegen = fs.readFileSync(configPath, 'utf8');
|
||||
assert(afterRegen.includes('openai-compatibility:'), 'Connector section should survive regeneration');
|
||||
assert(
|
||||
afterRegen.includes('openai-compatibility:'),
|
||||
'Connector section should survive regeneration'
|
||||
);
|
||||
assert(afterRegen.includes('name: mimo'), 'Connector name should survive regeneration');
|
||||
assert(
|
||||
afterRegen.includes('base-url: https://api.xiaomimimo.com/v1'),
|
||||
|
||||
@@ -71,11 +71,7 @@ describe('Tool Name Mapper', () => {
|
||||
|
||||
it('handles mixed valid and invalid names', () => {
|
||||
const mapper = new ToolNameMapper();
|
||||
const tools = [
|
||||
{ name: 'valid_tool' },
|
||||
{ name: 'foo__bar__bar' },
|
||||
{ name: 'another_valid' },
|
||||
];
|
||||
const tools = [{ name: 'valid_tool' }, { name: 'foo__bar__bar' }, { name: 'another_valid' }];
|
||||
|
||||
const result = mapper.registerTools(tools);
|
||||
|
||||
@@ -92,9 +88,7 @@ describe('Tool Name Mapper', () => {
|
||||
const mapper = new ToolNameMapper();
|
||||
mapper.registerTools([{ name: 'foo__bar__bar' }]);
|
||||
|
||||
const content = [
|
||||
{ type: 'tool_use', id: 'call_1', name: 'foo__bar', input: {} },
|
||||
];
|
||||
const content = [{ type: 'tool_use', id: 'call_1', name: 'foo__bar', input: {} }];
|
||||
|
||||
const result = mapper.restoreToolUse(content);
|
||||
|
||||
@@ -121,9 +115,7 @@ describe('Tool Name Mapper', () => {
|
||||
const mapper = new ToolNameMapper();
|
||||
mapper.registerTools([{ name: 'foo__bar__bar' }]);
|
||||
|
||||
const content = [
|
||||
{ type: 'tool_use', id: 'call_1', name: 'unknown_tool', input: {} },
|
||||
];
|
||||
const content = [{ type: 'tool_use', id: 'call_1', name: 'unknown_tool', input: {} }];
|
||||
|
||||
const result = mapper.restoreToolUse(content);
|
||||
|
||||
@@ -196,10 +188,7 @@ describe('Tool Name Mapper', () => {
|
||||
|
||||
it('returns list of all changes', () => {
|
||||
const mapper = new ToolNameMapper();
|
||||
mapper.registerTools([
|
||||
{ name: 'foo__bar__bar' },
|
||||
{ name: 'baz__qux__qux' },
|
||||
]);
|
||||
mapper.registerTools([{ name: 'foo__bar__bar' }, { name: 'baz__qux__qux' }]);
|
||||
|
||||
const changes = mapper.getChanges();
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { getManagedModelPrefix } from '../../shared/cliproxy-model-routing';
|
||||
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import {
|
||||
buildManagementHeaders,
|
||||
buildProxyUrl,
|
||||
getProxyTarget,
|
||||
} from '../proxy/proxy-target-resolver';
|
||||
import { mapExternalProviderName } from '../provider-capabilities';
|
||||
import type { CLIProxyProvider } from '../types';
|
||||
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
getTokenExpiryTimestamp,
|
||||
isTokenExpired,
|
||||
sanitizeEmail,
|
||||
} from '../../auth/auth-utils';
|
||||
import { getTokenExpiryTimestamp, isTokenExpired, sanitizeEmail } from '../../auth/auth-utils';
|
||||
|
||||
describe('Auth Utilities', () => {
|
||||
describe('sanitizeEmail', () => {
|
||||
|
||||
@@ -148,11 +148,9 @@ describe('findNewTokenSnapshotForManualAuth', () => {
|
||||
const newFile = path.join(tokenDir, 'kiro-new.json');
|
||||
fs.writeFileSync(newFile, JSON.stringify({ type: 'kiro', email: 'new@example.com' }));
|
||||
|
||||
const snapshot = findNewTokenSnapshotForManualAuth(
|
||||
'kiro',
|
||||
tokenDir,
|
||||
[{ file: 'kiro-existing.json', mtimeMs: existingMtimeMs }]
|
||||
);
|
||||
const snapshot = findNewTokenSnapshotForManualAuth('kiro', tokenDir, [
|
||||
{ file: 'kiro-existing.json', mtimeMs: existingMtimeMs },
|
||||
]);
|
||||
|
||||
expect(snapshot?.file).toBe('kiro-new.json');
|
||||
fs.rmSync(tokenDir, { recursive: true, force: true });
|
||||
@@ -168,7 +166,10 @@ describe('findNewTokenSnapshotForManualAuth', () => {
|
||||
`../oauth-handler?manual-auth-updated-token=${Date.now()}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(tokenFile, JSON.stringify({ type: 'kiro', email: 'existing@example.com', refreshed: true }));
|
||||
fs.writeFileSync(
|
||||
tokenFile,
|
||||
JSON.stringify({ type: 'kiro', email: 'existing@example.com', refreshed: true })
|
||||
);
|
||||
const bumpedTime = new Date(existingMtimeMs + 10_000);
|
||||
fs.utimesSync(tokenFile, bumpedTime, bumpedTime);
|
||||
|
||||
@@ -231,7 +232,9 @@ describe('getCliAuthNicknameError', () => {
|
||||
];
|
||||
|
||||
expect(getCliAuthNicknameError('kiro', 'work', existingAccounts, 'github-ABC123')).toBeNull();
|
||||
expect(getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')).toBeNull();
|
||||
expect(
|
||||
getCliAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -43,12 +43,7 @@ export {
|
||||
} from '.';
|
||||
|
||||
// Re-export environment detection functions
|
||||
export {
|
||||
isHeadlessEnvironment,
|
||||
killProcessOnPort,
|
||||
getTimeoutTroubleshooting,
|
||||
showStep,
|
||||
} from '.';
|
||||
export { isHeadlessEnvironment, killProcessOnPort, getTimeoutTroubleshooting, showStep } from '.';
|
||||
|
||||
// Re-export OAuth handling functions
|
||||
export { triggerOAuth, ensureAuth } from '.';
|
||||
|
||||
@@ -48,8 +48,7 @@ describe('deleteBinary ETXTBSY guard', () => {
|
||||
// We can't reliably trigger ETXTBSY in tests (need a running Go binary),
|
||||
// so we verify the code structure matches the expected behavior.
|
||||
const err = Object.assign(new Error('ETXTBSY: text file busy'), { code: 'ETXTBSY' });
|
||||
const code =
|
||||
err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
|
||||
const code = err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
|
||||
expect(code).toBe('ETXTBSY');
|
||||
// The guard only catches ETXTBSY, not EBUSY
|
||||
expect(code === 'ETXTBSY').toBe(true);
|
||||
@@ -59,8 +58,7 @@ describe('deleteBinary ETXTBSY guard', () => {
|
||||
it('EBUSY is not treated as "binary in use"', () => {
|
||||
// Verify that EBUSY (Windows mount/directory) is distinguished from ETXTBSY
|
||||
const err = Object.assign(new Error('EBUSY: resource busy'), { code: 'EBUSY' });
|
||||
const code =
|
||||
err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
|
||||
const code = err instanceof Error && 'code' in err ? (err as { code: string }).code : '';
|
||||
expect(code).toBe('EBUSY');
|
||||
expect(code === 'ETXTBSY').toBe(false);
|
||||
});
|
||||
|
||||
@@ -52,9 +52,7 @@ describe('installCliproxyVersion', () => {
|
||||
});
|
||||
|
||||
it('returns plus and emits an informational notice when plus backend is resolved locally', async () => {
|
||||
const binaryManager = await import(
|
||||
`../../binary-manager?binary-manager-warning=${Date.now()}`
|
||||
);
|
||||
const binaryManager = await import(`../../binary-manager?binary-manager-warning=${Date.now()}`);
|
||||
|
||||
const writes: string[] = [];
|
||||
const originalWrite = process.stderr.write.bind(process.stderr);
|
||||
@@ -146,9 +144,7 @@ describe('installCliproxyVersion', () => {
|
||||
ensureBinary: 0,
|
||||
};
|
||||
|
||||
const binaryManager = await import(
|
||||
`../../binary-manager?binary-manager-install=${Date.now()}`
|
||||
);
|
||||
const binaryManager = await import(`../../binary-manager?binary-manager-install=${Date.now()}`);
|
||||
|
||||
await binaryManager.installCliproxyVersion('6.7.1', false, 'plus', {
|
||||
createManager: () => ({
|
||||
@@ -181,9 +177,7 @@ describe('installCliproxyVersion', () => {
|
||||
});
|
||||
|
||||
it('fails fast when runtime startup forbids installing a missing binary', async () => {
|
||||
const binaryManager = await import(
|
||||
`../../binary-manager?binary-manager-runtime=${Date.now()}`
|
||||
);
|
||||
const binaryManager = await import(`../../binary-manager?binary-manager-runtime=${Date.now()}`);
|
||||
|
||||
await expect(
|
||||
binaryManager.ensureCLIProxyBinary(false, {
|
||||
|
||||
@@ -28,10 +28,7 @@ describe('version-checker stale cache fallback', () => {
|
||||
});
|
||||
|
||||
it('uses a stale latest-version cache when GitHub lookup fails', async () => {
|
||||
const {
|
||||
getVersionCachePath,
|
||||
writeInstalledVersion,
|
||||
} = await import('../version-cache');
|
||||
const { getVersionCachePath, writeInstalledVersion } = await import('../version-cache');
|
||||
const { VERSION_CACHE_DURATION_MS } = await import('../types');
|
||||
const { checkForUpdates } = await import('../version-checker');
|
||||
const plusBinDir = path.join(tempHome, '.ccs', 'cliproxy', 'bin', 'plus');
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as zlib from 'zlib';
|
||||
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../binary/platform-detector';
|
||||
import {
|
||||
getExecutableName,
|
||||
getArchiveBinaryName,
|
||||
DEFAULT_BACKEND,
|
||||
} from '../binary/platform-detector';
|
||||
import type { CLIProxyBackend } from '../types';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as zlib from 'zlib';
|
||||
import { getExecutableName, getArchiveBinaryName, DEFAULT_BACKEND } from '../binary/platform-detector';
|
||||
import {
|
||||
getExecutableName,
|
||||
getArchiveBinaryName,
|
||||
DEFAULT_BACKEND,
|
||||
} from '../binary/platform-detector';
|
||||
import type { CLIProxyBackend } from '../types';
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,9 @@ describe('Default Port Sync', () => {
|
||||
});
|
||||
|
||||
test('Device code providers are synced between backend and UI', () => {
|
||||
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(sorted(getProvidersByOAuthFlow('device_code')));
|
||||
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(
|
||||
sorted(getProvidersByOAuthFlow('device_code'))
|
||||
);
|
||||
});
|
||||
|
||||
test('plus-extra providers are synced between backend and UI', () => {
|
||||
@@ -60,13 +62,17 @@ describe('Default Port Sync', () => {
|
||||
|
||||
test('Provider display names are synced between backend and UI', () => {
|
||||
for (const provider of BACKEND_CLIPROXY_PROVIDER_IDS) {
|
||||
expect(UI_PROVIDER_METADATA[provider].displayName).toBe(getBackendProviderDisplayName(provider));
|
||||
expect(UI_PROVIDER_METADATA[provider].displayName).toBe(
|
||||
getBackendProviderDisplayName(provider)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Provider descriptions are synced between backend and UI', () => {
|
||||
for (const provider of BACKEND_CLIPROXY_PROVIDER_IDS) {
|
||||
expect(UI_PROVIDER_METADATA[provider].description).toBe(getBackendProviderDescription(provider));
|
||||
expect(UI_PROVIDER_METADATA[provider].description).toBe(
|
||||
getBackendProviderDescription(provider)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,8 +77,15 @@ describe('Config Generator Port', function () {
|
||||
it('returns config.yaml for default port (8317)', function () {
|
||||
const configPath = getConfigPathForPort(CLIPROXY_DEFAULT_PORT);
|
||||
const filename = path.basename(configPath);
|
||||
assert.ok(configPath.endsWith('config.yaml'), `Expected path to end with config.yaml but got: ${configPath}`);
|
||||
assert.strictEqual(filename, 'config.yaml', `Expected filename to be config.yaml but got: ${filename}`);
|
||||
assert.ok(
|
||||
configPath.endsWith('config.yaml'),
|
||||
`Expected path to end with config.yaml but got: ${configPath}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
filename,
|
||||
'config.yaml',
|
||||
`Expected filename to be config.yaml but got: ${filename}`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns config-{port}.yaml for variant ports', function () {
|
||||
|
||||
@@ -473,7 +473,10 @@ openai-compatibility:
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
assert(newConfig.includes('openai-compatibility:'), 'Should preserve openai-compatibility');
|
||||
assert(newConfig.includes('name: mimo'), 'Should preserve connector name');
|
||||
assert(newConfig.includes('base-url: https://api.xiaomimimo.com/v1'), 'Should preserve base URL');
|
||||
assert(
|
||||
newConfig.includes('base-url: https://api.xiaomimimo.com/v1'),
|
||||
'Should preserve base URL'
|
||||
);
|
||||
assert(newConfig.includes('alias: mimo-v2-flash'), 'Should preserve model aliases');
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ 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 {
|
||||
ensureProviderSettings,
|
||||
getEffectiveEnvVars,
|
||||
} from '../env-builder';
|
||||
import { ensureProviderSettings, getEffectiveEnvVars } from '../env-builder';
|
||||
|
||||
interface EnvSettings {
|
||||
ANTHROPIC_BASE_URL: string;
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
isProviderError,
|
||||
detectFailedTier,
|
||||
PROVIDER_ERROR_PATTERNS,
|
||||
} from '../retry-handler';
|
||||
import { isProviderError, detectFailedTier, PROVIDER_ERROR_PATTERNS } from '../retry-handler';
|
||||
import { applyFallback } from '../env-resolver';
|
||||
import { CompositeTierConfig } from '../../../config/unified-config-types';
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import {
|
||||
createCompositeVariant,
|
||||
updateCompositeVariant,
|
||||
} from '../../services/variant-service';
|
||||
import { createCompositeVariant, updateCompositeVariant } from '../../services/variant-service';
|
||||
import {
|
||||
saveCompositeVariantUnified,
|
||||
listVariantsFromConfig,
|
||||
|
||||
@@ -2,10 +2,7 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import {
|
||||
buildClaudeEnvironment,
|
||||
resolveCliproxyImageAnalysisEnv,
|
||||
} from '../env-resolver';
|
||||
import { buildClaudeEnvironment, resolveCliproxyImageAnalysisEnv } from '../env-resolver';
|
||||
import type { ImageAnalysisStatus } from '../../../utils/hooks';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -2,15 +2,16 @@ import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
execClaudeWithCLIProxy,
|
||||
hasGitLabTokenLoginFlag,
|
||||
readOptionValue,
|
||||
} from '../index';
|
||||
import { execClaudeWithCLIProxy, hasGitLabTokenLoginFlag, readOptionValue } from '../index';
|
||||
|
||||
describe('readOptionValue', () => {
|
||||
it('parses split-token option values', () => {
|
||||
expect(readOptionValue(['--kiro-idc-start-url', 'https://d-123.awsapps.com/start'], '--kiro-idc-start-url')).toEqual({
|
||||
expect(
|
||||
readOptionValue(
|
||||
['--kiro-idc-start-url', 'https://d-123.awsapps.com/start'],
|
||||
'--kiro-idc-start-url'
|
||||
)
|
||||
).toEqual({
|
||||
present: true,
|
||||
value: 'https://d-123.awsapps.com/start',
|
||||
missingValue: false,
|
||||
@@ -78,7 +79,9 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
||||
await execClaudeWithCLIProxy(fakeClaudePath, 'gemini', ['--browser', '--no-browser'], {});
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith('[X] Use either `--browser` or `--no-browser`, not both.');
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'[X] Use either `--browser` or `--no-browser`, not both.'
|
||||
);
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
|
||||
@@ -164,10 +164,7 @@ describe('Variant Port Allocation', function () {
|
||||
assert.fail('Should have thrown error');
|
||||
} catch (err) {
|
||||
assert.ok(err.message.includes('100/100'), 'Should include variant count');
|
||||
assert.ok(
|
||||
err.message.includes('ccs cliproxy remove'),
|
||||
'Should include recovery hint'
|
||||
);
|
||||
assert.ok(err.message.includes('ccs cliproxy remove'), 'Should include recovery hint');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,10 @@ import { stripBrowserEnv, stripClaudeCodeEnv } from '../../utils/shell-executor'
|
||||
import { CodexReasoningProxy } from '../ai-providers/codex-reasoning-proxy';
|
||||
import { ToolSanitizationProxy } from '../proxy/tool-sanitization-proxy';
|
||||
import { HttpsTunnelProxy } from '../proxy/https-tunnel-proxy';
|
||||
import { MODEL_ENV_VAR_KEYS, normalizeModelIdForProvider } from '../ai-providers/model-id-normalizer';
|
||||
import {
|
||||
MODEL_ENV_VAR_KEYS,
|
||||
normalizeModelIdForProvider,
|
||||
} from '../ai-providers/model-id-normalizer';
|
||||
import type { ProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import { getEffectiveApiKey } from '../auth/auth-token-manager';
|
||||
import { isSettings, type Settings } from '../../types/config';
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
unregisterSession,
|
||||
stopProxy,
|
||||
} from '../session-tracker';
|
||||
import { detectRunningProxy, waitForProxyHealthy, reclaimOrphanedProxy } from '../proxy/proxy-detector';
|
||||
import {
|
||||
detectRunningProxy,
|
||||
waitForProxyHealthy,
|
||||
reclaimOrphanedProxy,
|
||||
} from '../proxy/proxy-detector';
|
||||
import { withStartupLock } from '../services/startup-lock';
|
||||
import { killProcessOnPort } from '../../utils/platform-commands';
|
||||
import { stopQuotaMonitor } from '../quota/quota-manager';
|
||||
|
||||
+16
-3
@@ -108,7 +108,11 @@ export {
|
||||
} from './ai-providers/codex-plan-compatibility';
|
||||
|
||||
// Executor
|
||||
export { execClaudeWithCLIProxy, isPortAvailable, findAvailablePort } from './executor/cliproxy-executor';
|
||||
export {
|
||||
execClaudeWithCLIProxy,
|
||||
isPortAvailable,
|
||||
findAvailablePort,
|
||||
} from './executor/cliproxy-executor';
|
||||
|
||||
// Authentication
|
||||
export type { AuthStatus } from './auth/auth-handler';
|
||||
@@ -167,7 +171,11 @@ export { ensureCliproxyService, stopCliproxyService, getServiceStatus } from './
|
||||
|
||||
// Proxy detector (unified detection with multiple fallbacks)
|
||||
export type { ProxyStatus, DetectionMethod } from './proxy/proxy-detector';
|
||||
export { detectRunningProxy, waitForProxyHealthy, reclaimOrphanedProxy } from './proxy/proxy-detector';
|
||||
export {
|
||||
detectRunningProxy,
|
||||
waitForProxyHealthy,
|
||||
reclaimOrphanedProxy,
|
||||
} from './proxy/proxy-detector';
|
||||
|
||||
// Startup lock (prevents race conditions between CCS processes)
|
||||
export type { LockResult } from './services/startup-lock';
|
||||
@@ -246,7 +254,12 @@ export {
|
||||
GEMINI_MAX_TOOL_NAME_LENGTH,
|
||||
} from './ai-providers/tool-name-sanitizer';
|
||||
|
||||
export type { Tool, ToolUseBlock, ContentBlock, SanitizationChange } from './ai-providers/tool-name-mapper';
|
||||
export type {
|
||||
Tool,
|
||||
ToolUseBlock,
|
||||
ContentBlock,
|
||||
SanitizationChange,
|
||||
} from './ai-providers/tool-name-mapper';
|
||||
export { ToolNameMapper } from './ai-providers/tool-name-mapper';
|
||||
|
||||
export type { ToolSanitizationProxyConfig } from './proxy/tool-sanitization-proxy';
|
||||
|
||||
@@ -32,9 +32,7 @@ describe('Antigravity quota failure metadata', () => {
|
||||
it('preserves entitlement evidence when project lookup fails before quota fetch', async () => {
|
||||
const moduleId = Date.now() + Math.random();
|
||||
const { fetchAccountQuota } = await import(`../../quota-fetcher?agy-early=${moduleId}`);
|
||||
const { getProviderAuthDir } = await import(
|
||||
`../../config-generator?agy-config=${moduleId}`
|
||||
);
|
||||
const { getProviderAuthDir } = await import(`../../config-generator?agy-config=${moduleId}`);
|
||||
const fs = await import('node:fs');
|
||||
const os = await import('node:os');
|
||||
const path = await import('node:path');
|
||||
@@ -88,9 +86,7 @@ describe('Antigravity quota failure metadata', () => {
|
||||
const { fetchAccountQuota } = await import(
|
||||
`../../quota-fetcher?agy-invalid-project=${moduleId}`
|
||||
);
|
||||
const { getProviderAuthDir } = await import(
|
||||
`../../config-generator?agy-config=${moduleId}`
|
||||
);
|
||||
const { getProviderAuthDir } = await import(`../../config-generator?agy-config=${moduleId}`);
|
||||
const fs = await import('node:fs');
|
||||
const os = await import('node:os');
|
||||
const path = await import('node:path');
|
||||
@@ -144,9 +140,7 @@ describe('Antigravity quota failure metadata', () => {
|
||||
const { fetchAccountQuota } = await import(
|
||||
`../../quota-fetcher?agy-invalid-models=${moduleId}`
|
||||
);
|
||||
const { getProviderAuthDir } = await import(
|
||||
`../../config-generator?agy-config=${moduleId}`
|
||||
);
|
||||
const { getProviderAuthDir } = await import(`../../config-generator?agy-config=${moduleId}`);
|
||||
const fs = await import('node:fs');
|
||||
const os = await import('node:os');
|
||||
const path = await import('node:path');
|
||||
|
||||
@@ -516,13 +516,17 @@ describe('Codex Quota Fetcher', () => {
|
||||
accountId = `workspace-${email}`,
|
||||
tokenFile?: string
|
||||
): void {
|
||||
createCodexAccount(email, {
|
||||
access_token: 'test-token',
|
||||
account_id: accountId,
|
||||
expired: '2099-01-01T00:00:00.000Z',
|
||||
createCodexAccount(
|
||||
email,
|
||||
type: 'codex',
|
||||
}, tokenFile);
|
||||
{
|
||||
access_token: 'test-token',
|
||||
account_id: accountId,
|
||||
expired: '2099-01-01T00:00:00.000Z',
|
||||
email,
|
||||
type: 'codex',
|
||||
},
|
||||
tokenFile
|
||||
);
|
||||
}
|
||||
|
||||
it('maps deactivated workspace 402 responses to structured metadata', async () => {
|
||||
@@ -573,11 +577,7 @@ describe('Codex Quota Fetcher', () => {
|
||||
'codex-kaidu.kd@gmail.com-free.json'
|
||||
);
|
||||
|
||||
registerAccount(
|
||||
'codex',
|
||||
'codex-04a0f049-kaidu.kd@gmail.com-team.json',
|
||||
'kaidu.kd@gmail.com'
|
||||
);
|
||||
registerAccount('codex', 'codex-04a0f049-kaidu.kd@gmail.com-team.json', 'kaidu.kd@gmail.com');
|
||||
const freeAccount = registerAccount(
|
||||
'codex',
|
||||
'codex-kaidu.kd@gmail.com-free.json',
|
||||
@@ -611,16 +611,8 @@ describe('Codex Quota Fetcher', () => {
|
||||
});
|
||||
|
||||
it('does not guess a duplicate-email Codex auth file when the registry entry is missing', async () => {
|
||||
createValidCodexAccount(
|
||||
'kaidu.kd@gmail.com',
|
||||
'workspace-team',
|
||||
'codex-legacy-slot-a.json'
|
||||
);
|
||||
createValidCodexAccount(
|
||||
'kaidu.kd@gmail.com',
|
||||
'workspace-free',
|
||||
'codex-legacy-slot-b.json'
|
||||
);
|
||||
createValidCodexAccount('kaidu.kd@gmail.com', 'workspace-team', 'codex-legacy-slot-a.json');
|
||||
createValidCodexAccount('kaidu.kd@gmail.com', 'workspace-free', 'codex-legacy-slot-b.json');
|
||||
|
||||
const fetchSpy = mock(() =>
|
||||
Promise.resolve(
|
||||
|
||||
@@ -66,9 +66,7 @@ describe('Gemini CLI Quota Fetcher', () => {
|
||||
fetchGeminiCliQuota,
|
||||
resolveGeminiCliProjectId,
|
||||
__testExports: geminiTestExports,
|
||||
} = await import(
|
||||
`../quota-fetcher-gemini-cli?gemini-quota-fetcher=${moduleVersion}`
|
||||
));
|
||||
} = await import(`../quota-fetcher-gemini-cli?gemini-quota-fetcher=${moduleVersion}`));
|
||||
({ getProviderAuthDir } = configGenerator);
|
||||
});
|
||||
|
||||
@@ -1000,5 +998,4 @@ describe('Gemini CLI Quota Fetcher', () => {
|
||||
).toContain('project');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,11 +11,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
startQuotaMonitor,
|
||||
stopQuotaMonitor,
|
||||
clearQuotaCache,
|
||||
} from '../quota-manager';
|
||||
import { startQuotaMonitor, stopQuotaMonitor, clearQuotaCache } from '../quota-manager';
|
||||
import { __testExports as executorTestExports } from '../../executor';
|
||||
|
||||
// Setup test isolation
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
type GeminiCliParsedBucket,
|
||||
} from './gemini-cli-quota-normalizer';
|
||||
import { mapExternalProviderName } from '../provider-capabilities';
|
||||
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import {
|
||||
buildManagementHeaders,
|
||||
buildProxyUrl,
|
||||
getProxyTarget,
|
||||
} from '../proxy/proxy-target-resolver';
|
||||
import type { GeminiCliQuotaResult, GeminiCliBucket } from './quota-types';
|
||||
import {
|
||||
buildProviderEntitlementEvidence,
|
||||
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
normalizeProviderTierId,
|
||||
} from '../auth/provider-entitlement-evidence';
|
||||
import type { ProviderEntitlementEvidence } from '../auth/provider-entitlement-types';
|
||||
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import {
|
||||
buildManagementHeaders,
|
||||
buildProxyUrl,
|
||||
getProxyTarget,
|
||||
} from '../proxy/proxy-target-resolver';
|
||||
|
||||
/** Individual model quota info */
|
||||
export interface ModelQuota {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { describe, expect, it } from 'bun:test';
|
||||
import type { ProxyTarget } from '../../proxy/proxy-target-resolver';
|
||||
|
||||
async function loadRoutingHttpModule() {
|
||||
return import(
|
||||
`../routing-strategy-http?test=${Date.now()}-${Math.random()}`
|
||||
) as Promise<typeof import('../routing-strategy-http')>;
|
||||
return import(`../routing-strategy-http?test=${Date.now()}-${Math.random()}`) as Promise<
|
||||
typeof import('../routing-strategy-http')
|
||||
>;
|
||||
}
|
||||
|
||||
describe('routing-strategy-http', () => {
|
||||
|
||||
@@ -23,9 +23,7 @@ function createDetail(overrides: Partial<CliproxyRequestDetail> = {}): CliproxyR
|
||||
};
|
||||
}
|
||||
|
||||
function createInternallyBucketedUsage(
|
||||
details: CliproxyRequestDetail[]
|
||||
): CliproxyUsageApiResponse {
|
||||
function createInternallyBucketedUsage(details: CliproxyRequestDetail[]): CliproxyUsageApiResponse {
|
||||
return {
|
||||
usage: {
|
||||
total_requests: details.length,
|
||||
@@ -225,7 +223,9 @@ describe('buildCliproxyStatsFromUsageResponse', () => {
|
||||
{ label: 'empty authFiles', authFiles: [] },
|
||||
{
|
||||
label: 'missing auth_index match',
|
||||
authFiles: [{ auth_index: 'other-auth-index', provider: 'codex', email: 'shared@example.com' }],
|
||||
authFiles: [
|
||||
{ auth_index: 'other-auth-index', provider: 'codex', email: 'shared@example.com' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'matching auth_index without provider metadata',
|
||||
@@ -236,7 +236,10 @@ describe('buildCliproxyStatsFromUsageResponse', () => {
|
||||
for (const scenario of scenarios) {
|
||||
const stats = buildCliproxyStatsFromUsageResponse(usage, { authFiles: scenario.authFiles });
|
||||
|
||||
expect(stats.accountStats['ccs-internal-managed:shared@example.com'], scenario.label).toMatchObject({
|
||||
expect(
|
||||
stats.accountStats['ccs-internal-managed:shared@example.com'],
|
||||
scenario.label
|
||||
).toMatchObject({
|
||||
provider: 'ccs-internal-managed',
|
||||
source: 'shared@example.com',
|
||||
successCount: 1,
|
||||
@@ -280,7 +283,9 @@ describe('buildCliproxyStatsFromUsageResponse', () => {
|
||||
successCount: 1,
|
||||
failureCount: 0,
|
||||
});
|
||||
expect(stats.accountStats['ccs-internal-managed:providerless-fallback@example.com']).toMatchObject({
|
||||
expect(
|
||||
stats.accountStats['ccs-internal-managed:providerless-fallback@example.com']
|
||||
).toMatchObject({
|
||||
provider: 'ccs-internal-managed',
|
||||
source: 'providerless-fallback@example.com',
|
||||
successCount: 0,
|
||||
|
||||
@@ -6,10 +6,7 @@ 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 {
|
||||
updateVariant,
|
||||
validateProviderBackend,
|
||||
} from '../variant-service';
|
||||
import { updateVariant, validateProviderBackend } from '../variant-service';
|
||||
import { loadOrCreateUnifiedConfig } from '../../../config/unified-config-loader';
|
||||
|
||||
describe('updateVariant - provider/model consistency', () => {
|
||||
@@ -84,7 +81,10 @@ cliproxy:
|
||||
function setConfiguredBackend(backend: 'original' | 'plus'): void {
|
||||
const configPath = path.join(tmpDir, 'config.yaml');
|
||||
const current = fs.readFileSync(configPath, 'utf-8');
|
||||
fs.writeFileSync(configPath, current.replace(/backend: (original|plus)/, `backend: ${backend}`));
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
current.replace(/backend: (original|plus)/, `backend: ${backend}`)
|
||||
);
|
||||
}
|
||||
|
||||
it('rejects provider change without model update', () => {
|
||||
|
||||
@@ -10,7 +10,11 @@ import type {
|
||||
RemoteThinkingSupport,
|
||||
} from '../management/management-api-types';
|
||||
import { getDeniedModelIdReasonForProvider } from '../ai-providers/model-id-normalizer';
|
||||
import { buildManagementHeaders, buildProxyUrl, getProxyTarget } from '../proxy/proxy-target-resolver';
|
||||
import {
|
||||
buildManagementHeaders,
|
||||
buildProxyUrl,
|
||||
getProxyTarget,
|
||||
} from '../proxy/proxy-target-resolver';
|
||||
|
||||
const CACHE_FILE_NAME = 'model-catalog-cache.json';
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
@@ -48,9 +48,14 @@ describe('types.ts backward compatibility', () => {
|
||||
|
||||
it('exports all binary types', () => {
|
||||
const cfg: BinaryManagerConfig = {
|
||||
version: '1.0.0', releaseUrl: '', binPath: '/tmp',
|
||||
maxRetries: 3, verbose: false, forceVersion: false,
|
||||
skipAutoUpdate: false, allowInstall: true,
|
||||
version: '1.0.0',
|
||||
releaseUrl: '',
|
||||
binPath: '/tmp',
|
||||
maxRetries: 3,
|
||||
verbose: false,
|
||||
forceVersion: false,
|
||||
skipAutoUpdate: false,
|
||||
allowInstall: true,
|
||||
};
|
||||
expect(cfg.version).toBe('1.0.0');
|
||||
const progress: DownloadProgress = { total: 100, downloaded: 50, percentage: 50 };
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { getAuthStatus, initializeAccounts, type AuthStatus } from '../../cliproxy/auth/auth-handler';
|
||||
import {
|
||||
getAuthStatus,
|
||||
initializeAccounts,
|
||||
type AuthStatus,
|
||||
} from '../../cliproxy/auth/auth-handler';
|
||||
import {
|
||||
checkRemoteProxy,
|
||||
type RemoteProxyClientConfig,
|
||||
type RemoteProxyStatus,
|
||||
} from '../../cliproxy/services/remote-proxy-client';
|
||||
import { fetchRemoteAuthStatus, type RemoteAuthStatus } from '../../cliproxy/services/remote-auth-fetcher';
|
||||
import {
|
||||
fetchRemoteAuthStatus,
|
||||
type RemoteAuthStatus,
|
||||
} from '../../cliproxy/services/remote-auth-fetcher';
|
||||
import { getProxyTarget, type ProxyTarget } from '../../cliproxy/proxy/proxy-target-resolver';
|
||||
import { getProviderDisplayName, isCLIProxyProvider } from '../../cliproxy/provider-capabilities';
|
||||
import { isCliproxyRunning } from '../../cliproxy/services/stats-fetcher';
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
* types compatible with the CCS analytics dashboard.
|
||||
*/
|
||||
|
||||
import type { CliproxyUsageApiResponse, CliproxyRequestDetail } from '../../cliproxy/services/stats-fetcher';
|
||||
import type {
|
||||
CliproxyUsageApiResponse,
|
||||
CliproxyRequestDetail,
|
||||
} from '../../cliproxy/services/stats-fetcher';
|
||||
import { calculateCost } from '../model-pricing';
|
||||
import type { ModelBreakdown, DailyUsage, HourlyUsage, MonthlyUsage } from './types';
|
||||
import { getModelsUsed, normalizeUsageProvider } from './model-identity';
|
||||
|
||||
+2
-1
@@ -56,6 +56,7 @@
|
||||
"tests",
|
||||
"lib",
|
||||
"scripts",
|
||||
"ui"
|
||||
"ui",
|
||||
"src/**/__tests__/**"
|
||||
]
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ListAiProvidersResult,
|
||||
UpsertAiProviderEntryInput,
|
||||
} from '../../../src/cliproxy/ai-providers';
|
||||
import type { ProviderEntitlementEvidence } from '../../../src/cliproxy/provider-entitlement-types';
|
||||
import type { ProviderEntitlementEvidence } from '../../../src/cliproxy/auth/provider-entitlement-types';
|
||||
import type { BrowserRuntimeEnv } from '../../../src/utils/browser/chrome-reuse';
|
||||
|
||||
export const API_BASE_URL = '/api';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"default-ports.d.ts","sourceRoot":"","sources":["default-ports.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,eAAO,MAAM,mBAAmB,QAAQ,CAAC"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"default-ports.js","sourceRoot":"","sources":["default-ports.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEU,QAAA,qBAAqB,GAAG,IAAI,CAAC;AAC7B,QAAA,mBAAmB,GAAG,KAAK,CAAC"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["i18n.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AA29T3B,eAAe,IAAI,CAAC"}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["locales.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,eAAO,MAAM,iBAAiB,sCAAuC,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,SAAS,CAErE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAM5E;AAED,wBAAgB,eAAe,IAAI,SAAS,GAAG,IAAI,CAQlD;AAED,wBAAgB,gBAAgB,IAAI,SAAS,CAK5C;AAED,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAUvD"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"locales.js","sourceRoot":"","sources":["locales.ts"],"names":[],"mappings":";;;AAAa,QAAA,kBAAkB,GAAG,eAAe,CAAC;AAErC,QAAA,iBAAiB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAU,CAAC;AAItE,SAAgB,iBAAiB,CAAC,MAAc;IAC9C,OAAO,yBAAiB,CAAC,QAAQ,CAAC,MAAmB,CAAC,CAAC;AACzD,CAAC;AAFD,8CAEC;AAED,SAAgB,eAAe,CAAC,MAAiC;IAC/D,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IAC1D,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,IAAI,CAAC;AACd,CAAC;AAND,0CAMC;AAED,SAAgB,eAAe;IAC7B,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,0BAAkB,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AARD,0CAQC;AAED,SAAgB,gBAAgB;IAC9B,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,OAAO,SAAS,KAAK,WAAW;QAAE,OAAO,eAAe,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC;AACd,CAAC;AALD,4CAKC;AAED,SAAgB,mBAAmB,CAAC,MAAe;IACjD,IAAI,MAAM;QAAE,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,OAAO,SAAS,KAAK,WAAW;QAAE,OAAO,eAAe,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACjF,OAAO,IAAI,CAAC;AACd,CAAC;AAND,kDAMC;AAED,SAAgB,aAAa,CAAC,MAAc;IAC1C,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,0BAAkB,EAAE,UAAU,CAAC,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;QACxD,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAVD,sCAUC"}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"provider-config.d.ts","sourceRoot":"","sources":["provider-config.ts"],"names":[],"mappings":"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"}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user