mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
style: format source and test files
This commit is contained in:
@@ -94,9 +94,8 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise<void>
|
||||
* Fix image analysis configuration issues
|
||||
*/
|
||||
export async function fixImageAnalysisConfig(): Promise<boolean> {
|
||||
const { updateUnifiedConfig, loadOrCreateUnifiedConfig } = await import(
|
||||
'../../config/unified-config-loader'
|
||||
);
|
||||
const { updateUnifiedConfig, loadOrCreateUnifiedConfig } =
|
||||
await import('../../config/unified-config-loader');
|
||||
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
let fixed = false;
|
||||
|
||||
@@ -32,7 +32,8 @@ const MOCK_PORT = 59876; // Use a unique port for mock server
|
||||
const CLIPROXY_API_KEY = 'test-api-key-12345';
|
||||
|
||||
// Default provider models for testing (matches DEFAULT_IMAGE_ANALYSIS_CONFIG)
|
||||
const DEFAULT_PROVIDER_MODELS = 'agy:gemini-2.5-flash,gemini:gemini-2.5-flash,codex:gpt-5.1-codex-mini,kiro:kiro-claude-haiku-4-5,ghcp:claude-haiku-4.5,claude:claude-haiku-4-5-20251001';
|
||||
const DEFAULT_PROVIDER_MODELS =
|
||||
'agy:gemini-2.5-flash,gemini:gemini-2.5-flash,codex:gpt-5.1-codex-mini,kiro:kiro-claude-haiku-4-5,ghcp:claude-haiku-4.5,claude:claude-haiku-4-5-20251001';
|
||||
const DEFAULT_PROVIDER = 'agy'; // Default test provider
|
||||
|
||||
// ============================================================================
|
||||
@@ -168,13 +169,75 @@ function invokeHook(
|
||||
function createTestPng(filepath: string): void {
|
||||
// 1x1 PNG with a red pixel (RGB: 255, 0, 0)
|
||||
const png = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
|
||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
|
||||
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, // IDAT chunk (red pixel)
|
||||
0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d,
|
||||
0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, // IEND chunk
|
||||
0x44, 0xae, 0x42, 0x60, 0x82,
|
||||
0x89,
|
||||
0x50,
|
||||
0x4e,
|
||||
0x47,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x1a,
|
||||
0x0a, // PNG signature
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52, // IHDR chunk
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x90,
|
||||
0x77,
|
||||
0x53,
|
||||
0xde,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0c,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41, // IDAT chunk (red pixel)
|
||||
0x54,
|
||||
0x08,
|
||||
0xd7,
|
||||
0x63,
|
||||
0xf8,
|
||||
0xcf,
|
||||
0xc0,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x01,
|
||||
0x01,
|
||||
0x00,
|
||||
0x18,
|
||||
0xdd,
|
||||
0x8d,
|
||||
0xb4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4e, // IEND chunk
|
||||
0x44,
|
||||
0xae,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
]);
|
||||
fs.writeFileSync(filepath, png);
|
||||
}
|
||||
@@ -487,15 +550,14 @@ describe('Image Analyzer Hook', () => {
|
||||
expect(result.code).toBe(2);
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output.decision).toBe('block');
|
||||
expect(output.hookSpecificOutput.permissionDecisionReason).toContain(
|
||||
'CLIProxy unavailable'
|
||||
);
|
||||
expect(output.hookSpecificOutput.permissionDecisionReason).toContain('CLIProxy unavailable');
|
||||
});
|
||||
|
||||
it('should analyze PNG via mock CLIProxy and return analysis', () => {
|
||||
resetMockState();
|
||||
mockResponse = {
|
||||
content: 'This image shows a small red square, likely a single pixel or very minimal graphic.',
|
||||
content:
|
||||
'This image shows a small red square, likely a single pixel or very minimal graphic.',
|
||||
statusCode: 200,
|
||||
};
|
||||
|
||||
|
||||
@@ -100,12 +100,14 @@ describe('ProfileDetector', () => {
|
||||
const mockUnifiedConfig = {
|
||||
version: 2,
|
||||
profiles: {
|
||||
glm: { settings: settingsPath, type: 'api' }
|
||||
}
|
||||
glm: { settings: settingsPath, type: 'api' },
|
||||
},
|
||||
};
|
||||
|
||||
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
|
||||
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(mockUnifiedConfig as any);
|
||||
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
|
||||
mockUnifiedConfig as any
|
||||
);
|
||||
|
||||
try {
|
||||
const result = detector.detectProfileType('glm');
|
||||
@@ -122,12 +124,14 @@ 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' },
|
||||
},
|
||||
};
|
||||
|
||||
const isUnifiedModeSpy = spyOn(unifiedConfigLoader, 'isUnifiedMode').mockReturnValue(true);
|
||||
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(mockUnifiedConfig as any);
|
||||
const loadUnifiedConfigSpy = spyOn(unifiedConfigLoader, 'loadUnifiedConfig').mockReturnValue(
|
||||
mockUnifiedConfig as any
|
||||
);
|
||||
|
||||
try {
|
||||
const result = detector.detectProfileType('work');
|
||||
|
||||
@@ -13,7 +13,11 @@ 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 { handleQuotaExhaustion, writeQuotaWarning, maskEmail } from '../../../src/cliproxy/account-safety';
|
||||
import {
|
||||
handleQuotaExhaustion,
|
||||
writeQuotaWarning,
|
||||
maskEmail,
|
||||
} from '../../../src/cliproxy/account-safety';
|
||||
|
||||
// Setup test isolation
|
||||
let tmpDir: string;
|
||||
|
||||
@@ -128,8 +128,7 @@ describe('detectFailedTier', () => {
|
||||
});
|
||||
|
||||
it('should match first tier when multiple models mentioned', () => {
|
||||
const stderr =
|
||||
'Tried claude-opus-4-6-thinking, then gemini-3-pro-preview, both failed';
|
||||
const stderr = 'Tried claude-opus-4-6-thinking, then gemini-3-pro-preview, both failed';
|
||||
const result = detectFailedTier(stderr, tiers);
|
||||
expect(result).toBe('opus'); // First match
|
||||
});
|
||||
|
||||
@@ -57,9 +57,7 @@ describe('shouldApplyExtendedContext', () => {
|
||||
});
|
||||
|
||||
it('returns false for Claude models without explicit flag', () => {
|
||||
expect(shouldApplyExtendedContext('agy', 'claude-opus-4-5-thinking', undefined)).toBe(
|
||||
false
|
||||
);
|
||||
expect(shouldApplyExtendedContext('agy', 'claude-opus-4-5-thinking', undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for Claude models without explicit flag', () => {
|
||||
|
||||
@@ -427,9 +427,7 @@ describe('management-api-client', () => {
|
||||
describe('CRUD operations', () => {
|
||||
it('should get claude keys', async () => {
|
||||
const client = new ManagementApiClient(config);
|
||||
const mockKeys: ClaudeKey[] = [
|
||||
{ 'api-key': 'sk-test-123', prefix: 'glm-' },
|
||||
];
|
||||
const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-123', prefix: 'glm-' }];
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
global.fetch = mock(() =>
|
||||
@@ -449,9 +447,7 @@ describe('management-api-client', () => {
|
||||
|
||||
it('should put claude keys', async () => {
|
||||
const client = new ManagementApiClient(config);
|
||||
const mockKeys: ClaudeKey[] = [
|
||||
{ 'api-key': 'sk-test-456', prefix: 'kimi-' },
|
||||
];
|
||||
const mockKeys: ClaudeKey[] = [{ 'api-key': 'sk-test-456', prefix: 'kimi-' }];
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
let requestBody: string | undefined;
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
validatePort,
|
||||
CLIPROXY_DEFAULT_PORT,
|
||||
} from '../../../src/cliproxy/config-generator';
|
||||
import { validatePort, CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config-generator';
|
||||
import { resolveProxyConfig } from '../../../src/cliproxy/proxy-config-resolver';
|
||||
|
||||
describe('Port Validation', () => {
|
||||
|
||||
@@ -67,9 +67,7 @@ describe('Gemini CLI Quota Fetcher', () => {
|
||||
});
|
||||
|
||||
it('should handle camelCase API response', () => {
|
||||
const rawBuckets = [
|
||||
{ modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 },
|
||||
];
|
||||
const rawBuckets = [{ modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 }];
|
||||
|
||||
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ 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 '../../../src/cliproxy/quota-manager';
|
||||
import {
|
||||
startQuotaMonitor,
|
||||
stopQuotaMonitor,
|
||||
clearQuotaCache,
|
||||
} from '../../../src/cliproxy/quota-manager';
|
||||
|
||||
// Setup test isolation
|
||||
let tmpDir: string;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* Unit tests for remote-proxy-client module
|
||||
*/
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import type { RemoteProxyClientConfig, RemoteProxyStatus } from '../../../src/cliproxy/remote-proxy-client';
|
||||
import type {
|
||||
RemoteProxyClientConfig,
|
||||
RemoteProxyStatus,
|
||||
} from '../../../src/cliproxy/remote-proxy-client';
|
||||
|
||||
// We test the module's type exports and error handling logic
|
||||
// Actual HTTP calls are not mocked in this unit test - use integration tests for that
|
||||
|
||||
@@ -535,10 +535,7 @@ describe('sanitizeToolSchemas', () => {
|
||||
});
|
||||
|
||||
test('handles tools without input_schema', () => {
|
||||
const tools = [
|
||||
{ name: 'simple_tool', description: 'No schema' },
|
||||
{ name: 'another_tool' },
|
||||
];
|
||||
const tools = [{ name: 'simple_tool', description: 'No schema' }, { name: 'another_tool' }];
|
||||
|
||||
const result = sanitizeToolSchemas(tools);
|
||||
|
||||
|
||||
@@ -111,7 +111,10 @@ describe('ToolSanitizationProxy Integration', () => {
|
||||
body: JSON.stringify({
|
||||
model: 'test-model',
|
||||
tools: [
|
||||
{ name: 'gitmcp__plus-pro-components__plus-pro-components', description: 'Test tool' },
|
||||
{
|
||||
name: 'gitmcp__plus-pro-components__plus-pro-components',
|
||||
description: 'Test tool',
|
||||
},
|
||||
{ name: 'valid_tool', description: 'Valid tool' },
|
||||
],
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
@@ -465,11 +468,7 @@ describe('ToolSanitizationProxy Integration', () => {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tools: [
|
||||
{ name: 'tool_a__x__x' },
|
||||
{ name: 'tool_b__y__y' },
|
||||
{ name: 'tool_c_valid' },
|
||||
],
|
||||
tools: [{ name: 'tool_a__x__x' }, { name: 'tool_b__y__y' }, { name: 'tool_c_valid' }],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -77,15 +77,11 @@ describe('env-command', () => {
|
||||
});
|
||||
|
||||
it('formats powershell export', () => {
|
||||
expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe(
|
||||
"$env:API_KEY = 'sk-123'"
|
||||
);
|
||||
expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe("$env:API_KEY = 'sk-123'");
|
||||
});
|
||||
|
||||
it('escapes single quotes in values', () => {
|
||||
expect(formatExportLine('bash', 'VAL', "it's here")).toBe(
|
||||
"export VAL='it'\\''s here'"
|
||||
);
|
||||
expect(formatExportLine('bash', 'VAL', "it's here")).toBe("export VAL='it'\\''s here'");
|
||||
});
|
||||
|
||||
it('handles empty values', () => {
|
||||
@@ -104,9 +100,7 @@ describe('env-command', () => {
|
||||
});
|
||||
|
||||
it('prevents backtick injection in values', () => {
|
||||
expect(formatExportLine('bash', 'TOKEN', 'safe`whoami`')).toBe(
|
||||
"export TOKEN='safe`whoami`'"
|
||||
);
|
||||
expect(formatExportLine('bash', 'TOKEN', 'safe`whoami`')).toBe("export TOKEN='safe`whoami`'");
|
||||
});
|
||||
|
||||
it('escapes single quotes in fish values', () => {
|
||||
@@ -114,9 +108,7 @@ describe('env-command', () => {
|
||||
});
|
||||
|
||||
it('escapes single quotes in powershell values', () => {
|
||||
expect(formatExportLine('powershell', 'VAL', "it's here")).toBe(
|
||||
"$env:VAL = 'it''s here'"
|
||||
);
|
||||
expect(formatExportLine('powershell', 'VAL', "it's here")).toBe("$env:VAL = 'it''s here'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,11 +197,15 @@ describe('env-command', () => {
|
||||
});
|
||||
|
||||
it('returns undefined when no positional args', () => {
|
||||
expect(findProfile(['--format', 'openai', '--shell', 'fish'], ['format', 'shell'])).toBeUndefined();
|
||||
expect(
|
||||
findProfile(['--format', 'openai', '--shell', 'fish'], ['format', 'shell'])
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips multiple flag-value pairs', () => {
|
||||
expect(findProfile(['--format', 'openai', '--shell', 'fish', 'codex'], ['format', 'shell'])).toBe('codex');
|
||||
expect(
|
||||
findProfile(['--format', 'openai', '--shell', 'fish', 'codex'], ['format', 'shell'])
|
||||
).toBe('codex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,8 @@ describe('isFirstTimeInstall logic', () => {
|
||||
createConfigJson({ profiles: { glm: '~/.ccs/glm.settings.json' } });
|
||||
|
||||
const legacyConfig = JSON.parse(fs.readFileSync(path.join(testDir, 'config.json'), 'utf8'));
|
||||
const hasLegacyProfiles = legacyConfig.profiles && Object.keys(legacyConfig.profiles).length > 0;
|
||||
const hasLegacyProfiles =
|
||||
legacyConfig.profiles && Object.keys(legacyConfig.profiles).length > 0;
|
||||
|
||||
expect(hasLegacyProfiles).toBe(true);
|
||||
});
|
||||
@@ -48,8 +49,11 @@ describe('isFirstTimeInstall logic', () => {
|
||||
createConfigYaml('version: 2\nprofiles: {}\naccounts: {}');
|
||||
createProfilesJson({ profiles: { work: { path: '/some/path' } } });
|
||||
|
||||
const legacyProfiles = JSON.parse(fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8'));
|
||||
const hasLegacyAccounts = legacyProfiles.profiles && Object.keys(legacyProfiles.profiles).length > 0;
|
||||
const legacyProfiles = JSON.parse(
|
||||
fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8')
|
||||
);
|
||||
const hasLegacyAccounts =
|
||||
legacyProfiles.profiles && Object.keys(legacyProfiles.profiles).length > 0;
|
||||
|
||||
expect(hasLegacyAccounts).toBe(true);
|
||||
});
|
||||
@@ -105,7 +109,9 @@ cliproxy_server:
|
||||
createProfilesJson({ profiles: {} });
|
||||
|
||||
const legacyConfig = JSON.parse(fs.readFileSync(path.join(testDir, 'config.json'), 'utf8'));
|
||||
const legacyProfiles = JSON.parse(fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8'));
|
||||
const legacyProfiles = JSON.parse(
|
||||
fs.readFileSync(path.join(testDir, 'profiles.json'), 'utf8')
|
||||
);
|
||||
|
||||
const hasLegacyProfiles = Object.keys(legacyConfig.profiles || {}).length > 0;
|
||||
const hasLegacyAccounts = Object.keys(legacyProfiles.profiles || {}).length > 0;
|
||||
|
||||
@@ -135,6 +135,8 @@ describe('shell-completion command', () => {
|
||||
await expect(handleShellCompletionCommand([])).rejects.toThrow('process.exit(1)');
|
||||
const plainErrorLines = errorLines.map(stripAnsi);
|
||||
expect(plainErrorLines.some((line) => line.includes('Error: boom'))).toBe(true);
|
||||
expect(plainErrorLines.some((line) => line.includes('ccs --shell-completion --zsh'))).toBe(true);
|
||||
expect(plainErrorLines.some((line) => line.includes('ccs --shell-completion --zsh'))).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -141,39 +141,35 @@ describe('startDaemon', () => {
|
||||
expect(result.error).toContain('Invalid port');
|
||||
});
|
||||
|
||||
it(
|
||||
'starts and stops daemon successfully',
|
||||
async () => {
|
||||
const port = 10000 + Math.floor(Math.random() * 50000);
|
||||
const result = await startDaemon({ port, ghost_mode: true });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.pid).toBeDefined();
|
||||
it('starts and stops daemon successfully', async () => {
|
||||
const port = 10000 + Math.floor(Math.random() * 50000);
|
||||
const result = await startDaemon({ port, ghost_mode: true });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.pid).toBeDefined();
|
||||
|
||||
// Verify health
|
||||
const running = await isDaemonRunning(port);
|
||||
expect(running).toBe(true);
|
||||
// Verify health
|
||||
const running = await isDaemonRunning(port);
|
||||
expect(running).toBe(true);
|
||||
|
||||
// Verify chat endpoint exists (requires auth, should not be 404)
|
||||
const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4.1',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
}),
|
||||
});
|
||||
expect(chatResponse.status).toBe(401);
|
||||
// Verify chat endpoint exists (requires auth, should not be 404)
|
||||
const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4.1',
|
||||
messages: [{ role: 'user', content: 'hello' }],
|
||||
}),
|
||||
});
|
||||
expect(chatResponse.status).toBe(401);
|
||||
|
||||
// Stop
|
||||
const stopResult = await stopDaemon();
|
||||
expect(stopResult.success).toBe(true);
|
||||
// Stop
|
||||
const stopResult = await stopDaemon();
|
||||
expect(stopResult.success).toBe(true);
|
||||
|
||||
// Verify stopped
|
||||
const stillRunning = await isDaemonRunning(port);
|
||||
expect(stillRunning).toBe(false);
|
||||
},
|
||||
35000
|
||||
);
|
||||
// Verify stopped
|
||||
const stillRunning = await isDaemonRunning(port);
|
||||
expect(stillRunning).toBe(false);
|
||||
}, 35000);
|
||||
});
|
||||
|
||||
describe('isDaemonRunning', () => {
|
||||
|
||||
@@ -300,7 +300,9 @@ describe('Message Translation', () => {
|
||||
|
||||
expect(result.messages).toHaveLength(1);
|
||||
expect(result.messages[0].role).toBe('user');
|
||||
expect(result.messages[0].content).toBe('[System Instructions]\nSystem instruction part 1 part 2');
|
||||
expect(result.messages[0].content).toBe(
|
||||
'[System Instructions]\nSystem instruction part 1 part 2'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -332,7 +334,12 @@ describe('Request Encoding', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateCursorBody([{ role: 'user', content: 'What is the weather?' }], 'gpt-4', tools, null);
|
||||
const result = generateCursorBody(
|
||||
[{ role: 'user', content: 'What is the weather?' }],
|
||||
'gpt-4',
|
||||
tools,
|
||||
null
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
@@ -358,7 +365,9 @@ describe('Request Encoding', () => {
|
||||
const executor = new CursorExecutor();
|
||||
|
||||
// Frame header says payload is 100 bytes but only 5 bytes follow
|
||||
const truncatedFrame = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05]);
|
||||
const truncatedFrame = Buffer.from([
|
||||
0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
]);
|
||||
|
||||
const result = executor.transformProtobufToJSON(truncatedFrame, 'gpt-4', {
|
||||
messages: [],
|
||||
@@ -644,7 +653,10 @@ describe('CursorExecutor', () => {
|
||||
];
|
||||
|
||||
// buildCursorRequest expects (model, body, stream, credentials)
|
||||
buildCursorRequest('test-model', { messages }, false, { machineId: '12345', accessToken: 'test' });
|
||||
buildCursorRequest('test-model', { messages }, false, {
|
||||
machineId: '12345',
|
||||
accessToken: 'test',
|
||||
});
|
||||
|
||||
// Should have logged warning
|
||||
const hasWarning = consoleSpy.some((log) => log.includes('Unknown message role'));
|
||||
|
||||
@@ -14,9 +14,7 @@ import { type RawUsageEntry } from '../../src/web-server/jsonl-parser';
|
||||
// TEST FIXTURES
|
||||
// ============================================================================
|
||||
|
||||
const createEntry = (
|
||||
overrides: Partial<RawUsageEntry> = {}
|
||||
): RawUsageEntry => ({
|
||||
const createEntry = (overrides: Partial<RawUsageEntry> = {}): RawUsageEntry => ({
|
||||
inputTokens: 1000,
|
||||
outputTokens: 500,
|
||||
cacheCreationTokens: 100,
|
||||
@@ -66,9 +64,7 @@ describe('aggregateDailyUsage', () => {
|
||||
expect(result[0].modelsUsed).toContain('claude-opus-4-5-20251101');
|
||||
|
||||
// Find sonnet breakdown
|
||||
const sonnet = result[0].modelBreakdowns.find(
|
||||
(b) => b.modelName === 'claude-sonnet-4-5'
|
||||
);
|
||||
const sonnet = result[0].modelBreakdowns.find((b) => b.modelName === 'claude-sonnet-4-5');
|
||||
expect(sonnet!.inputTokens).toBe(1500); // 1000 + 500
|
||||
});
|
||||
|
||||
|
||||
@@ -124,13 +124,7 @@ describe('DelegationHandler', () => {
|
||||
|
||||
describe('_extractOptions - agents JSON validation', () => {
|
||||
it('accepts valid JSON for agents', () => {
|
||||
const options = handler._extractOptions([
|
||||
'glm',
|
||||
'-p',
|
||||
'test',
|
||||
'--agents',
|
||||
'{"name":"test"}',
|
||||
]);
|
||||
const options = handler._extractOptions(['glm', '-p', 'test', '--agents', '{"name":"test"}']);
|
||||
expect(options.agents).toBe('{"name":"test"}');
|
||||
});
|
||||
|
||||
@@ -153,7 +147,13 @@ describe('DelegationHandler', () => {
|
||||
|
||||
describe('_extractOptions - betas validation', () => {
|
||||
it('accepts valid betas value', () => {
|
||||
const options = handler._extractOptions(['glm', '-p', 'test', '--betas', 'feature1,feature2']);
|
||||
const options = handler._extractOptions([
|
||||
'glm',
|
||||
'-p',
|
||||
'test',
|
||||
'--betas',
|
||||
'feature1,feature2',
|
||||
]);
|
||||
expect(options.betas).toBe('feature1,feature2');
|
||||
});
|
||||
|
||||
@@ -165,13 +165,7 @@ describe('DelegationHandler', () => {
|
||||
|
||||
describe('_extractOptions - extraArgs passthrough', () => {
|
||||
it('passes unknown flags through to extraArgs', () => {
|
||||
const options = handler._extractOptions([
|
||||
'glm',
|
||||
'-p',
|
||||
'test',
|
||||
'--unknown-flag',
|
||||
'value',
|
||||
]);
|
||||
const options = handler._extractOptions(['glm', '-p', 'test', '--unknown-flag', 'value']);
|
||||
expect(options.extraArgs).toContain('--unknown-flag');
|
||||
expect(options.extraArgs).toContain('value');
|
||||
});
|
||||
|
||||
@@ -10,10 +10,7 @@ describe('HeadlessExecutor flag construction', () => {
|
||||
// Since HeadlessExecutor.execute() spawns a process, we test the logic directly
|
||||
|
||||
describe('Duplicate flag filtering', () => {
|
||||
function filterExtraArgs(
|
||||
extraArgs: string[],
|
||||
explicitFlags: Set<string>
|
||||
): string[] {
|
||||
function filterExtraArgs(extraArgs: string[], explicitFlags: Set<string>): string[] {
|
||||
const filteredExtras: string[] = [];
|
||||
for (let i = 0; i < extraArgs.length; i++) {
|
||||
if (explicitFlags.has(extraArgs[i])) {
|
||||
|
||||
@@ -30,11 +30,13 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
// Helper to create proxy instance with specific config
|
||||
async function createTestableProxy(config: {
|
||||
maxRetries?: number;
|
||||
baseDelay?: number;
|
||||
enabled?: boolean;
|
||||
} = {}) {
|
||||
async function createTestableProxy(
|
||||
config: {
|
||||
maxRetries?: number;
|
||||
baseDelay?: number;
|
||||
enabled?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
// Set env vars before import
|
||||
if (config.maxRetries !== undefined) {
|
||||
process.env.GLMT_MAX_RETRIES = String(config.maxRetries);
|
||||
@@ -56,7 +58,11 @@ describe('GLMT Retry Logic', () => {
|
||||
it('should use default values when env vars not set', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
// Access private via type assertion for testing
|
||||
const config = (proxy as unknown as { retryConfig: { maxRetries: number; baseDelay: number; enabled: boolean } }).retryConfig;
|
||||
const config = (
|
||||
proxy as unknown as {
|
||||
retryConfig: { maxRetries: number; baseDelay: number; enabled: boolean };
|
||||
}
|
||||
).retryConfig;
|
||||
expect(config.maxRetries).toBe(3);
|
||||
expect(config.baseDelay).toBe(1000);
|
||||
expect(config.enabled).toBe(true);
|
||||
@@ -84,7 +90,11 @@ describe('GLMT Retry Logic', () => {
|
||||
describe('calculateRetryDelay', () => {
|
||||
it('should calculate exponential delay with jitter', async () => {
|
||||
const proxy = await createTestableProxy({ baseDelay: 1000 });
|
||||
const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy);
|
||||
const calcDelay = (
|
||||
proxy as unknown as {
|
||||
calculateRetryDelay: (attempt: number, retryAfter?: string) => number;
|
||||
}
|
||||
).calculateRetryDelay.bind(proxy);
|
||||
|
||||
// Attempt 0: 2^0 * 1000 = 1000 + jitter (0-500)
|
||||
const delay0 = calcDelay(0);
|
||||
@@ -104,7 +114,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should honor Retry-After header in seconds', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy);
|
||||
const calcDelay = (
|
||||
proxy as unknown as {
|
||||
calculateRetryDelay: (attempt: number, retryAfter?: string) => number;
|
||||
}
|
||||
).calculateRetryDelay.bind(proxy);
|
||||
|
||||
// Retry-After: 5 seconds → 5000ms
|
||||
const delay = calcDelay(0, '5');
|
||||
@@ -113,7 +127,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should ignore invalid Retry-After header and fallback to exponential', async () => {
|
||||
const proxy = await createTestableProxy({ baseDelay: 1000 });
|
||||
const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy);
|
||||
const calcDelay = (
|
||||
proxy as unknown as {
|
||||
calculateRetryDelay: (attempt: number, retryAfter?: string) => number;
|
||||
}
|
||||
).calculateRetryDelay.bind(proxy);
|
||||
|
||||
// Invalid header falls back to exponential
|
||||
const delay = calcDelay(0, 'invalid');
|
||||
@@ -123,7 +141,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should ignore zero or negative Retry-After', async () => {
|
||||
const proxy = await createTestableProxy({ baseDelay: 1000 });
|
||||
const calcDelay = (proxy as unknown as { calculateRetryDelay: (attempt: number, retryAfter?: string) => number }).calculateRetryDelay.bind(proxy);
|
||||
const calcDelay = (
|
||||
proxy as unknown as {
|
||||
calculateRetryDelay: (attempt: number, retryAfter?: string) => number;
|
||||
}
|
||||
).calculateRetryDelay.bind(proxy);
|
||||
|
||||
const delay = calcDelay(0, '0');
|
||||
expect(delay).toBeGreaterThanOrEqual(1000);
|
||||
@@ -134,7 +156,11 @@ describe('GLMT Retry Logic', () => {
|
||||
describe('isRetryableError', () => {
|
||||
it('should return true for 429 status code', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy);
|
||||
const isRetryable = (
|
||||
proxy as unknown as {
|
||||
isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string };
|
||||
}
|
||||
).isRetryableError.bind(proxy);
|
||||
|
||||
const result = isRetryable(new Error('Upstream error: 429 Too Many Requests'));
|
||||
expect(result.retryable).toBe(true);
|
||||
@@ -142,7 +168,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should return true for rate limit message', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy);
|
||||
const isRetryable = (
|
||||
proxy as unknown as {
|
||||
isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string };
|
||||
}
|
||||
).isRetryableError.bind(proxy);
|
||||
|
||||
const result = isRetryable(new Error('Rate limit exceeded'));
|
||||
expect(result.retryable).toBe(true);
|
||||
@@ -150,7 +180,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should return false for non-retryable errors', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy);
|
||||
const isRetryable = (
|
||||
proxy as unknown as {
|
||||
isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string };
|
||||
}
|
||||
).isRetryableError.bind(proxy);
|
||||
|
||||
expect(isRetryable(new Error('Connection refused')).retryable).toBe(false);
|
||||
expect(isRetryable(new Error('Timeout')).retryable).toBe(false);
|
||||
@@ -160,7 +194,11 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should extract Retry-After from error message', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const isRetryable = (proxy as unknown as { isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string } }).isRetryableError.bind(proxy);
|
||||
const isRetryable = (
|
||||
proxy as unknown as {
|
||||
isRetryableError: (error: Error) => { retryable: boolean; retryAfter?: string };
|
||||
}
|
||||
).isRetryableError.bind(proxy);
|
||||
|
||||
const result = isRetryable(new Error('429 Too Many Requests, Retry-After: 10'));
|
||||
expect(result.retryable).toBe(true);
|
||||
@@ -174,47 +212,66 @@ describe('GLMT Retry Logic', () => {
|
||||
let attempts = 0;
|
||||
|
||||
// Mock forwardToUpstream
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream = async () => {
|
||||
attempts++;
|
||||
return { choices: [{ message: { content: 'success' } }] };
|
||||
};
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream =
|
||||
async () => {
|
||||
attempts++;
|
||||
return { choices: [{ message: { content: 'success' } }] };
|
||||
};
|
||||
|
||||
const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown> }).forwardWithRetry.bind(proxy);
|
||||
const forwardWithRetry = (
|
||||
proxy as unknown as {
|
||||
forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown>;
|
||||
}
|
||||
).forwardWithRetry.bind(proxy);
|
||||
const result = await forwardWithRetry({}, {});
|
||||
|
||||
expect(attempts).toBe(1);
|
||||
expect((result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content).toBe('success');
|
||||
expect(
|
||||
(result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content
|
||||
).toBe('success');
|
||||
});
|
||||
|
||||
it('should retry on 429 and succeed eventually', async () => {
|
||||
const proxy = await createTestableProxy({ baseDelay: 10 }); // Fast for tests
|
||||
let attempts = 0;
|
||||
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream = async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) {
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
}
|
||||
return { choices: [{ message: { content: 'success after retry' } }] };
|
||||
};
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream =
|
||||
async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) {
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
}
|
||||
return { choices: [{ message: { content: 'success after retry' } }] };
|
||||
};
|
||||
|
||||
const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown> }).forwardWithRetry.bind(proxy);
|
||||
const forwardWithRetry = (
|
||||
proxy as unknown as {
|
||||
forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown>;
|
||||
}
|
||||
).forwardWithRetry.bind(proxy);
|
||||
const result = await forwardWithRetry({}, {});
|
||||
|
||||
expect(attempts).toBe(3);
|
||||
expect((result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content).toBe('success after retry');
|
||||
expect(
|
||||
(result as { choices: Array<{ message: { content: string } }> }).choices[0].message.content
|
||||
).toBe('success after retry');
|
||||
});
|
||||
|
||||
it('should fail after max retries exhausted', async () => {
|
||||
const proxy = await createTestableProxy({ maxRetries: 2, baseDelay: 10 });
|
||||
let attempts = 0;
|
||||
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream = async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
};
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream =
|
||||
async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
};
|
||||
|
||||
const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown> }).forwardWithRetry.bind(proxy);
|
||||
const forwardWithRetry = (
|
||||
proxy as unknown as {
|
||||
forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown>;
|
||||
}
|
||||
).forwardWithRetry.bind(proxy);
|
||||
|
||||
await expect(forwardWithRetry({}, {})).rejects.toThrow('429');
|
||||
expect(attempts).toBe(3); // Initial + 2 retries
|
||||
@@ -224,12 +281,17 @@ describe('GLMT Retry Logic', () => {
|
||||
const proxy = await createTestableProxy({ enabled: false });
|
||||
let attempts = 0;
|
||||
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream = async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
};
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream =
|
||||
async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 429 Too Many Requests');
|
||||
};
|
||||
|
||||
const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown> }).forwardWithRetry.bind(proxy);
|
||||
const forwardWithRetry = (
|
||||
proxy as unknown as {
|
||||
forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown>;
|
||||
}
|
||||
).forwardWithRetry.bind(proxy);
|
||||
|
||||
await expect(forwardWithRetry({}, {})).rejects.toThrow('429');
|
||||
expect(attempts).toBe(1);
|
||||
@@ -239,12 +301,17 @@ describe('GLMT Retry Logic', () => {
|
||||
const proxy = await createTestableProxy({ baseDelay: 10 });
|
||||
let attempts = 0;
|
||||
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream = async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 500 Internal Server Error');
|
||||
};
|
||||
(proxy as unknown as { forwardToUpstream: () => Promise<unknown> }).forwardToUpstream =
|
||||
async () => {
|
||||
attempts++;
|
||||
throw new Error('Upstream error: 500 Internal Server Error');
|
||||
};
|
||||
|
||||
const forwardWithRetry = (proxy as unknown as { forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown> }).forwardWithRetry.bind(proxy);
|
||||
const forwardWithRetry = (
|
||||
proxy as unknown as {
|
||||
forwardWithRetry: (req: unknown, headers: unknown) => Promise<unknown>;
|
||||
}
|
||||
).forwardWithRetry.bind(proxy);
|
||||
|
||||
await expect(forwardWithRetry({}, {})).rejects.toThrow('500');
|
||||
expect(attempts).toBe(1);
|
||||
@@ -254,7 +321,8 @@ describe('GLMT Retry Logic', () => {
|
||||
describe('connection pooling', () => {
|
||||
it('should create https.Agent with keepAlive enabled', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const agent = (proxy as unknown as { httpsAgent: { options?: { keepAlive?: boolean } } }).httpsAgent;
|
||||
const agent = (proxy as unknown as { httpsAgent: { options?: { keepAlive?: boolean } } })
|
||||
.httpsAgent;
|
||||
|
||||
expect(agent).toBeDefined();
|
||||
// Agent should have keepAlive behavior (internal property)
|
||||
@@ -263,7 +331,9 @@ describe('GLMT Retry Logic', () => {
|
||||
|
||||
it('should destroy agent on stop', async () => {
|
||||
const proxy = await createTestableProxy();
|
||||
const agent = (proxy as unknown as { httpsAgent: { destroy: () => void; destroyed?: boolean } }).httpsAgent;
|
||||
const agent = (
|
||||
proxy as unknown as { httpsAgent: { destroy: () => void; destroyed?: boolean } }
|
||||
).httpsAgent;
|
||||
|
||||
let destroyed = false;
|
||||
const originalDestroy = agent.destroy.bind(agent);
|
||||
|
||||
@@ -219,14 +219,7 @@ describe('parseJsonlFile', () => {
|
||||
|
||||
test('handles file with blank lines', async () => {
|
||||
const filePath = path.join(tempDir, 'blanks.jsonl');
|
||||
const content = [
|
||||
'',
|
||||
VALID_ASSISTANT_ENTRY,
|
||||
'',
|
||||
' ',
|
||||
ASSISTANT_ENTRY_NO_CACHE,
|
||||
'',
|
||||
].join('\n');
|
||||
const content = ['', VALID_ASSISTANT_ENTRY, '', ' ', ASSISTANT_ENTRY_NO_CACHE, ''].join('\n');
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('mcp-manager logic', () => {
|
||||
|
||||
it('should not detect unrelated MCPs', () => {
|
||||
expect(detectWebSearchMcp({ 'my-custom-mcp': {} })).toBe(false);
|
||||
expect(detectWebSearchMcp({ 'filesystem': {} })).toBe(false);
|
||||
expect(detectWebSearchMcp({ filesystem: {} })).toBe(false);
|
||||
expect(detectWebSearchMcp({ 'github-copilot': {} })).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,8 @@ describe('SharedManager', () => {
|
||||
'claude-hud@claude-hud': [
|
||||
{
|
||||
scope: 'user',
|
||||
installPath: '/home/kai/.ccs/instances/ck/plugins/cache/claude-hud/claude-hud/0.0.2',
|
||||
installPath:
|
||||
'/home/kai/.ccs/instances/ck/plugins/cache/claude-hud/claude-hud/0.0.2',
|
||||
version: '0.0.2',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -101,9 +101,7 @@ describe('droid-config-manager', () => {
|
||||
provider: 'anthropic',
|
||||
});
|
||||
|
||||
const settings = JSON.parse(
|
||||
fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')
|
||||
);
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
||||
expect(settings.customModels).toHaveLength(2);
|
||||
expect(settings.customModels[0].displayName).toBe('My GPT');
|
||||
expect(settings.customModels[1].displayName).toBe('CCS gemini');
|
||||
@@ -135,9 +133,7 @@ describe('droid-config-manager', () => {
|
||||
provider: 'anthropic',
|
||||
});
|
||||
|
||||
const settings = JSON.parse(
|
||||
fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')
|
||||
);
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
||||
expect(settings.customModels).toHaveLength(2);
|
||||
expect(settings.customModels[0].provider).toBe('custom-provider');
|
||||
expect(settings.customModels[1].displayName).toBe('CCS gemini');
|
||||
@@ -162,7 +158,10 @@ describe('droid-config-manager', () => {
|
||||
it('should reject symlinked temp file path', async () => {
|
||||
const factoryDir = path.join(tmpDir, '.factory');
|
||||
fs.mkdirSync(factoryDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(factoryDir, 'settings.json'), JSON.stringify({ customModels: [] }));
|
||||
fs.writeFileSync(
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({ customModels: [] })
|
||||
);
|
||||
fs.symlinkSync('/tmp', path.join(factoryDir, 'settings.json.tmp'));
|
||||
|
||||
await expect(
|
||||
@@ -202,9 +201,7 @@ describe('droid-config-manager', () => {
|
||||
provider: 'anthropic',
|
||||
});
|
||||
|
||||
const settings = JSON.parse(
|
||||
fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8')
|
||||
);
|
||||
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
||||
expect(settings.customModels).toHaveLength(1);
|
||||
expect(settings.customModels[0].displayName).toBe('CCS gemini');
|
||||
expect(settings.customModels[0].apiKey).toBe('new-key');
|
||||
@@ -255,8 +252,20 @@ describe('droid-config-manager', () => {
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [
|
||||
{ model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' },
|
||||
{ model: 'opus', displayName: 'CCS gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{
|
||||
model: 'gpt-4o',
|
||||
displayName: 'My GPT',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'openai',
|
||||
},
|
||||
{
|
||||
model: 'opus',
|
||||
displayName: 'CCS gemini',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -275,8 +284,20 @@ describe('droid-config-manager', () => {
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [
|
||||
{ model: 'opus', displayName: 'ccs-gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{ model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' },
|
||||
{
|
||||
model: 'opus',
|
||||
displayName: 'ccs-gemini',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
{
|
||||
model: 'gpt-4o',
|
||||
displayName: 'My GPT',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'openai',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -351,7 +372,12 @@ describe('droid-config-manager', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [null, 123, 'bad', { displayName: 'CCS ok', model: 'x', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' }],
|
||||
customModels: [
|
||||
null,
|
||||
123,
|
||||
'bad',
|
||||
{ displayName: 'CCS ok', model: 'x', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
@@ -458,8 +484,20 @@ describe('droid-config-manager', () => {
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [
|
||||
{ model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' },
|
||||
{ model: 'opus', displayName: 'CCS old-profile', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{
|
||||
model: 'gpt-4o',
|
||||
displayName: 'My GPT',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'openai',
|
||||
},
|
||||
{
|
||||
model: 'opus',
|
||||
displayName: 'CCS old-profile',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -479,8 +517,20 @@ describe('droid-config-manager', () => {
|
||||
path.join(factoryDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
customModels: [
|
||||
{ model: 'opus', displayName: 'ccs-gemini', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{ model: 'sonnet', displayName: 'ccs-codex', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{
|
||||
model: 'opus',
|
||||
displayName: 'ccs-gemini',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
{
|
||||
model: 'sonnet',
|
||||
displayName: 'ccs-codex',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'anthropic',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -502,7 +552,13 @@ describe('droid-config-manager', () => {
|
||||
customModels: [
|
||||
{ model: 'x', displayName: 'CCS ', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{ model: 'x', displayName: 'ccs-', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
||||
{ model: 'gpt-4o', displayName: 'My GPT', baseUrl: 'x', apiKey: 'y', provider: 'openai' },
|
||||
{
|
||||
model: 'gpt-4o',
|
||||
displayName: 'My GPT',
|
||||
baseUrl: 'x',
|
||||
apiKey: 'y',
|
||||
provider: 'openai',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
@@ -517,31 +573,27 @@ describe('droid-config-manager', () => {
|
||||
});
|
||||
|
||||
describe('concurrent writes', () => {
|
||||
it(
|
||||
'should handle concurrent upserts without data loss',
|
||||
async () => {
|
||||
const profiles = Array.from({ length: 10 }, (_, i) => `profile-${i}`);
|
||||
it('should handle concurrent upserts without data loss', async () => {
|
||||
const profiles = Array.from({ length: 10 }, (_, i) => `profile-${i}`);
|
||||
|
||||
await Promise.all(
|
||||
profiles.map((p) =>
|
||||
upsertCcsModel(p, {
|
||||
model: 'test-model',
|
||||
displayName: `CCS ${p}`,
|
||||
baseUrl: 'http://localhost:8317',
|
||||
apiKey: 'key',
|
||||
provider: 'anthropic',
|
||||
})
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
profiles.map((p) =>
|
||||
upsertCcsModel(p, {
|
||||
model: 'test-model',
|
||||
displayName: `CCS ${p}`,
|
||||
baseUrl: 'http://localhost:8317',
|
||||
apiKey: 'key',
|
||||
provider: 'anthropic',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const models = await listCcsModels();
|
||||
expect(models.size).toBe(10);
|
||||
const models = await listCcsModels();
|
||||
expect(models.size).toBe(10);
|
||||
|
||||
for (const p of profiles) {
|
||||
expect(models.has(p)).toBe(true);
|
||||
}
|
||||
},
|
||||
15000
|
||||
);
|
||||
for (const p of profiles) {
|
||||
expect(models.has(p)).toBe(true);
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,9 +129,9 @@ describe('stripTargetFlag', () => {
|
||||
});
|
||||
|
||||
it('should remove repeated --target flags', () => {
|
||||
expect(stripTargetFlag(['--target', 'droid', 'gemini', '--target=claude', '--verbose'])).toEqual(
|
||||
['gemini', '--verbose']
|
||||
);
|
||||
expect(
|
||||
stripTargetFlag(['--target', 'droid', 'gemini', '--target=claude', '--verbose'])
|
||||
).toEqual(['gemini', '--verbose']);
|
||||
});
|
||||
|
||||
it('should throw when --target has no value', () => {
|
||||
|
||||
@@ -19,7 +19,16 @@ import { isUnifiedConfigEnabled } from '../../src/config/feature-flags';
|
||||
// Inline helper to test secret key detection (utility kept for potential reuse)
|
||||
function isSecretKey(key: string): boolean {
|
||||
const upper = key.toUpperCase();
|
||||
const secretPatterns = ['TOKEN', 'SECRET', 'API_KEY', 'APIKEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'PRIVATE'];
|
||||
const secretPatterns = [
|
||||
'TOKEN',
|
||||
'SECRET',
|
||||
'API_KEY',
|
||||
'APIKEY',
|
||||
'PASSWORD',
|
||||
'CREDENTIAL',
|
||||
'AUTH',
|
||||
'PRIVATE',
|
||||
];
|
||||
return secretPatterns.some((pattern) => upper.includes(pattern));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,89 +1,91 @@
|
||||
import { expect, test, describe, beforeEach, afterEach } from "bun:test";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { expandPath } from "../../../src/utils/helpers";
|
||||
import { expect, test, describe, beforeEach, afterEach } from 'bun:test';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { expandPath } from '../../../src/utils/helpers';
|
||||
|
||||
describe("expandPath", () => {
|
||||
describe('expandPath', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
const HOME = os.homedir();
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.TEST_HOME = "/custom/home";
|
||||
process.env.TEST_VAR = "foo";
|
||||
process.env.TEST_HOME = '/custom/home';
|
||||
process.env.TEST_VAR = 'foo';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test("1. Tilde expansion: ~/path -> /home/user/path", () => {
|
||||
expect(expandPath("~/test/file.txt")).toBe(path.join(HOME, "test/file.txt"));
|
||||
test('1. Tilde expansion: ~/path -> /home/user/path', () => {
|
||||
expect(expandPath('~/test/file.txt')).toBe(path.join(HOME, 'test/file.txt'));
|
||||
});
|
||||
|
||||
test("2. Windows tilde with backslash: ~\\path", () => {
|
||||
test('2. Windows tilde with backslash: ~\\path', () => {
|
||||
// expandPath handles both ~/ and ~\ regardless of platform
|
||||
expect(expandPath("~\\test\\file.txt")).toBe(path.join(HOME, "test/file.txt"));
|
||||
expect(expandPath('~\\test\\file.txt')).toBe(path.join(HOME, 'test/file.txt'));
|
||||
});
|
||||
|
||||
test("3. Environment variable expansion: ${VAR}/path", () => {
|
||||
expect(expandPath("${TEST_HOME}/file.txt")).toBe(path.normalize("/custom/home/file.txt"));
|
||||
test('3. Environment variable expansion: ${VAR}/path', () => {
|
||||
expect(expandPath('${TEST_HOME}/file.txt')).toBe(path.normalize('/custom/home/file.txt'));
|
||||
});
|
||||
|
||||
test("4. Dollar sign env vars: $VAR/path", () => {
|
||||
expect(expandPath("$TEST_HOME/file.txt")).toBe(path.normalize("/custom/home/file.txt"));
|
||||
test('4. Dollar sign env vars: $VAR/path', () => {
|
||||
expect(expandPath('$TEST_HOME/file.txt')).toBe(path.normalize('/custom/home/file.txt'));
|
||||
});
|
||||
|
||||
test("5. Windows %VAR% expansion: %VAR%\\path (simulated)", () => {
|
||||
test('5. Windows %VAR% expansion: %VAR%\\path (simulated)', () => {
|
||||
// We can't easily mock process.platform if it's not win32,
|
||||
// but the function check process.platform === 'win32'
|
||||
if (process.platform === 'win32') {
|
||||
expect(expandPath("%TEST_HOME%\\file.txt")).toBe(path.normalize("/custom/home/file.txt"));
|
||||
expect(expandPath('%TEST_HOME%\\file.txt')).toBe(path.normalize('/custom/home/file.txt'));
|
||||
} else {
|
||||
// Should remain unchanged on non-windows (but separators normalized)
|
||||
expect(expandPath("%TEST_HOME%\\file.txt")).toBe(path.normalize("%TEST_HOME%/file.txt"));
|
||||
expect(expandPath('%TEST_HOME%\\file.txt')).toBe(path.normalize('%TEST_HOME%/file.txt'));
|
||||
}
|
||||
});
|
||||
|
||||
test("6. Mixed path separators normalization", () => {
|
||||
const result = expandPath("path/to\\some/file");
|
||||
expect(result).toBe(path.normalize("path/to/some/file"));
|
||||
test('6. Mixed path separators normalization', () => {
|
||||
const result = expandPath('path/to\\some/file');
|
||||
expect(result).toBe(path.normalize('path/to/some/file'));
|
||||
});
|
||||
|
||||
test("7. Nested env vars: ${HOME}/${VAR}/path", () => {
|
||||
expect(expandPath("${TEST_HOME}/${TEST_VAR}/file.txt")).toBe(path.normalize("/custom/home/foo/file.txt"));
|
||||
test('7. Nested env vars: ${HOME}/${VAR}/path', () => {
|
||||
expect(expandPath('${TEST_HOME}/${TEST_VAR}/file.txt')).toBe(
|
||||
path.normalize('/custom/home/foo/file.txt')
|
||||
);
|
||||
});
|
||||
|
||||
test("8. Empty/null path handling", () => {
|
||||
expect(expandPath("")).toBe(".");
|
||||
test('8. Empty/null path handling', () => {
|
||||
expect(expandPath('')).toBe('.');
|
||||
});
|
||||
|
||||
test("9. Already absolute paths stay unchanged", () => {
|
||||
const absPath = "/absolute/path";
|
||||
test('9. Already absolute paths stay unchanged', () => {
|
||||
const absPath = '/absolute/path';
|
||||
expect(expandPath(absPath)).toBe(path.normalize(absPath));
|
||||
});
|
||||
|
||||
test("10. Undefined env vars -> empty string", () => {
|
||||
expect(expandPath("${UNDEFINED_VAR}/file.txt")).toBe(path.normalize("/file.txt"));
|
||||
expect(expandPath("$UNDEFINED_VAR/file.txt")).toBe(path.normalize("/file.txt"));
|
||||
test('10. Undefined env vars -> empty string', () => {
|
||||
expect(expandPath('${UNDEFINED_VAR}/file.txt')).toBe(path.normalize('/file.txt'));
|
||||
expect(expandPath('$UNDEFINED_VAR/file.txt')).toBe(path.normalize('/file.txt'));
|
||||
});
|
||||
|
||||
test("11. Windows drive letters stay intact", () => {
|
||||
test('11. Windows drive letters stay intact', () => {
|
||||
// Windows drive letter paths should be preserved
|
||||
const result = expandPath("C:\\Users\\test\\file.txt");
|
||||
expect(result).toContain("Users");
|
||||
expect(result).toContain("test");
|
||||
const result = expandPath('C:\\Users\\test\\file.txt');
|
||||
expect(result).toContain('Users');
|
||||
expect(result).toContain('test');
|
||||
});
|
||||
|
||||
test("12. Windows UNC paths handled", () => {
|
||||
test('12. Windows UNC paths handled', () => {
|
||||
// UNC paths start with \\
|
||||
const uncPath = "\\\\server\\share\\folder";
|
||||
const uncPath = '\\\\server\\share\\folder';
|
||||
const result = expandPath(uncPath);
|
||||
// Should normalize but preserve the structure
|
||||
expect(result).toContain("server");
|
||||
expect(result).toContain("share");
|
||||
expect(result).toContain('server');
|
||||
expect(result).toContain('share');
|
||||
});
|
||||
|
||||
test("13. Null-like input throws TypeError", () => {
|
||||
test('13. Null-like input throws TypeError', () => {
|
||||
// Function requires string input - documents current behavior
|
||||
// @ts-ignore - testing runtime edge case
|
||||
expect(() => expandPath(undefined as unknown as string)).toThrow(TypeError);
|
||||
@@ -91,14 +93,14 @@ describe("expandPath", () => {
|
||||
expect(() => expandPath(null as unknown as string)).toThrow(TypeError);
|
||||
});
|
||||
|
||||
test("14. Path with spaces preserved", () => {
|
||||
const pathWithSpaces = "~/My Documents/file.txt";
|
||||
test('14. Path with spaces preserved', () => {
|
||||
const pathWithSpaces = '~/My Documents/file.txt';
|
||||
const result = expandPath(pathWithSpaces);
|
||||
expect(result).toContain("My Documents");
|
||||
expect(result).toContain('My Documents');
|
||||
});
|
||||
|
||||
test("15. Multiple consecutive slashes normalized", () => {
|
||||
const result = expandPath("path//to///file.txt");
|
||||
expect(result).toBe(path.normalize("path/to/file.txt"));
|
||||
test('15. Multiple consecutive slashes normalized', () => {
|
||||
const result = expandPath('path//to///file.txt');
|
||||
expect(result).toBe(path.normalize('path/to/file.txt'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,7 +97,9 @@ describe('signal-forwarder', () => {
|
||||
const child = createMockChildProcess();
|
||||
const before = getSignalListenerCounts();
|
||||
const onError = jest.fn(async () => {});
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
const err = Object.assign(new Error('spawn failed'), {
|
||||
code: 'ENOENT',
|
||||
}) as NodeJS.ErrnoException;
|
||||
|
||||
wireChildProcessSignals(child, onError);
|
||||
child.emit('error', err);
|
||||
@@ -113,7 +115,9 @@ describe('signal-forwarder', () => {
|
||||
const child = createMockChildProcess();
|
||||
const onError = jest.fn(async () => {});
|
||||
const onExit = jest.fn();
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
const err = Object.assign(new Error('spawn failed'), {
|
||||
code: 'ENOENT',
|
||||
}) as NodeJS.ErrnoException;
|
||||
|
||||
wireChildProcessSignals(child, onError, onExit);
|
||||
child.emit('error', err);
|
||||
@@ -134,7 +138,9 @@ describe('signal-forwarder', () => {
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
|
||||
try {
|
||||
const err = Object.assign(new Error('spawn failed'), { code: 'ENOENT' }) as NodeJS.ErrnoException;
|
||||
const err = Object.assign(new Error('spawn failed'), {
|
||||
code: 'ENOENT',
|
||||
}) as NodeJS.ErrnoException;
|
||||
wireChildProcessSignals(child, onError);
|
||||
child.emit('error', err);
|
||||
await Promise.resolve();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { expect, test, describe } from 'bun:test';
|
||||
import { isCcsWebSearchHook, deduplicateCcsHooks } from '../../../../src/utils/websearch/hook-utils';
|
||||
import {
|
||||
isCcsWebSearchHook,
|
||||
deduplicateCcsHooks,
|
||||
} from '../../../../src/utils/websearch/hook-utils';
|
||||
|
||||
describe('isCcsWebSearchHook', () => {
|
||||
test('Returns true for CCS hook with forward slashes (Unix path)', () => {
|
||||
|
||||
@@ -28,17 +28,13 @@ describe('Dashboard Auth', () => {
|
||||
|
||||
describe('getDashboardAuthConfig', () => {
|
||||
it('returns disabled by default', async () => {
|
||||
const { getDashboardAuthConfig } = await import(
|
||||
'../../src/config/unified-config-loader'
|
||||
);
|
||||
const { getDashboardAuthConfig } = await import('../../src/config/unified-config-loader');
|
||||
const config = getDashboardAuthConfig();
|
||||
expect(config.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns 24 hour default session timeout', async () => {
|
||||
const { getDashboardAuthConfig } = await import(
|
||||
'../../src/config/unified-config-loader'
|
||||
);
|
||||
const { getDashboardAuthConfig } = await import('../../src/config/unified-config-loader');
|
||||
const config = getDashboardAuthConfig();
|
||||
expect(config.session_timeout_hours).toBe(24);
|
||||
});
|
||||
@@ -89,12 +85,7 @@ describe('Dashboard Auth', () => {
|
||||
});
|
||||
|
||||
describe('public paths', () => {
|
||||
const PUBLIC_PATHS = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/check',
|
||||
'/api/auth/setup',
|
||||
'/api/health',
|
||||
];
|
||||
const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/check', '/api/auth/setup', '/api/health'];
|
||||
|
||||
it('identifies public paths correctly', () => {
|
||||
const isPublicPath = (path: string) =>
|
||||
|
||||
@@ -3,7 +3,9 @@ import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cli
|
||||
|
||||
describe('cliproxy-auth-routes start-url guard', () => {
|
||||
it('rejects device code providers', () => {
|
||||
expect(getStartUrlUnsupportedReason('kiro')).toContain("Kiro method 'aws' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('kiro')).toContain(
|
||||
"Kiro method 'aws' uses Device Code flow"
|
||||
);
|
||||
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||
});
|
||||
|
||||
@@ -14,7 +14,10 @@ process.env.CCS_HOME = TEST_CCS_DIR;
|
||||
|
||||
// Import after setting env var
|
||||
import type { CursorConfig } from '../../../src/config/unified-config-types';
|
||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||
import {
|
||||
loadOrCreateUnifiedConfig,
|
||||
saveUnifiedConfig,
|
||||
} from '../../../src/config/unified-config-loader';
|
||||
import { getCcsDir } from '../../../src/utils/config-manager';
|
||||
|
||||
describe('Cursor Settings Routes Logic', () => {
|
||||
|
||||
Reference in New Issue
Block a user