mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-06 01:54:11 +08:00
- #70: Fix API Key Tester URL handling - normalize trailing slashes before version suffix detection to prevent double-slash URLs like //models - #69: Fix memory embedder ignoring CodexLens config - add error handling for CodexLensConfig.load() with fallback to defaults - #68: Fix ccw cli using wrong Python environment - add getCodexLensVenvPython() to resolve correct venv path on Windows/Unix - #67: Fix LiteLLM API Provider test endpoint - actually test API key connection instead of just checking ccw-litellm installation - #66: Fix help-routes.ts path configuration - use correct 'ccw-help' directory name and refactor getIndexDir to pure function - #63: Fix CodexLens install state refresh - add cache invalidation after config save in codexlens-manager.js Also includes targeted unit tests for the URL normalization logic. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166 lines
6.5 KiB
TypeScript
166 lines
6.5 KiB
TypeScript
/**
|
|
* Unit tests for API Key Tester service (ccw/src/core/services/api-key-tester.ts)
|
|
*
|
|
* Tests URL construction logic, version suffix detection, and trailing slash handling.
|
|
* Uses Node's built-in test runner (node:test).
|
|
*/
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
// Import functions that don't require fetch
|
|
import { validateApiBaseUrl, getDefaultApiBase } from '../src/core/services/api-key-tester.js';
|
|
|
|
describe('API Key Tester', () => {
|
|
describe('validateApiBaseUrl', () => {
|
|
it('should accept valid HTTPS URLs', () => {
|
|
const result = validateApiBaseUrl('https://api.openai.com/v1');
|
|
assert.equal(result.valid, true);
|
|
});
|
|
|
|
it('should accept valid HTTP URLs (for local development)', () => {
|
|
const result = validateApiBaseUrl('http://localhost:8080');
|
|
assert.equal(result.valid, true);
|
|
});
|
|
|
|
it('should reject non-HTTP protocols', () => {
|
|
const result = validateApiBaseUrl('ftp://example.com');
|
|
assert.equal(result.valid, false);
|
|
assert.equal(result.error, 'URL must use HTTP or HTTPS protocol');
|
|
});
|
|
|
|
it('should reject invalid URL format', () => {
|
|
const result = validateApiBaseUrl('not-a-url');
|
|
assert.equal(result.valid, false);
|
|
assert.equal(result.error, 'Invalid URL format');
|
|
});
|
|
});
|
|
|
|
describe('getDefaultApiBase', () => {
|
|
it('should return OpenAI default for openai provider', () => {
|
|
assert.equal(getDefaultApiBase('openai'), 'https://api.openai.com/v1');
|
|
});
|
|
|
|
it('should return Anthropic default for anthropic provider', () => {
|
|
assert.equal(getDefaultApiBase('anthropic'), 'https://api.anthropic.com/v1');
|
|
});
|
|
|
|
it('should return OpenAI default for custom provider', () => {
|
|
assert.equal(getDefaultApiBase('custom'), 'https://api.openai.com/v1');
|
|
});
|
|
});
|
|
|
|
describe('URL Normalization Logic (Issue #70 fix verification)', () => {
|
|
// Test the regex pattern used in testApiKeyConnection
|
|
const normalizeUrl = (url: string) => url.replace(/\/+$/, '');
|
|
const hasVersionSuffix = (url: string) => /\/v\d+$/.test(url);
|
|
|
|
describe('Trailing slash removal', () => {
|
|
it('should remove single trailing slash', () => {
|
|
assert.equal(normalizeUrl('https://api.openai.com/v1/'), 'https://api.openai.com/v1');
|
|
});
|
|
|
|
it('should remove multiple trailing slashes', () => {
|
|
assert.equal(normalizeUrl('https://api.openai.com/v1///'), 'https://api.openai.com/v1');
|
|
});
|
|
|
|
it('should not modify URL without trailing slash', () => {
|
|
assert.equal(normalizeUrl('https://api.openai.com/v1'), 'https://api.openai.com/v1');
|
|
});
|
|
});
|
|
|
|
describe('Version suffix detection', () => {
|
|
it('should detect /v1 suffix', () => {
|
|
assert.equal(hasVersionSuffix('https://api.openai.com/v1'), true);
|
|
});
|
|
|
|
it('should detect /v2 suffix', () => {
|
|
assert.equal(hasVersionSuffix('https://api.custom.com/v2'), true);
|
|
});
|
|
|
|
it('should detect /v4 suffix (z.ai style)', () => {
|
|
assert.equal(hasVersionSuffix('https://api.z.ai/api/coding/paas/v4'), true);
|
|
});
|
|
|
|
it('should NOT detect version when URL has no version suffix', () => {
|
|
assert.equal(hasVersionSuffix('http://localhost:8080'), false);
|
|
});
|
|
|
|
it('should NOT detect version when followed by slash (before normalization)', () => {
|
|
// After normalization, the slash should be removed
|
|
assert.equal(hasVersionSuffix('https://api.openai.com/v1/'), false);
|
|
assert.equal(hasVersionSuffix(normalizeUrl('https://api.openai.com/v1/')), true);
|
|
});
|
|
});
|
|
|
|
describe('URL construction verification', () => {
|
|
const constructModelsUrl = (apiBase: string) => {
|
|
const normalized = normalizeUrl(apiBase);
|
|
return hasVersionSuffix(normalized) ? `${normalized}/models` : `${normalized}/v1/models`;
|
|
};
|
|
|
|
it('should construct correct URL for https://api.openai.com/v1', () => {
|
|
assert.equal(constructModelsUrl('https://api.openai.com/v1'), 'https://api.openai.com/v1/models');
|
|
});
|
|
|
|
it('should construct correct URL for https://api.openai.com/v1/ (with trailing slash)', () => {
|
|
assert.equal(constructModelsUrl('https://api.openai.com/v1/'), 'https://api.openai.com/v1/models');
|
|
});
|
|
|
|
it('should construct correct URL for https://api.custom.com/v2', () => {
|
|
assert.equal(constructModelsUrl('https://api.custom.com/v2'), 'https://api.custom.com/v2/models');
|
|
});
|
|
|
|
it('should construct correct URL for https://api.custom.com/v2/ (with trailing slash)', () => {
|
|
assert.equal(constructModelsUrl('https://api.custom.com/v2/'), 'https://api.custom.com/v2/models');
|
|
});
|
|
|
|
it('should construct correct URL for https://api.z.ai/api/coding/paas/v4', () => {
|
|
assert.equal(constructModelsUrl('https://api.z.ai/api/coding/paas/v4'), 'https://api.z.ai/api/coding/paas/v4/models');
|
|
});
|
|
|
|
it('should add /v1 when no version suffix: http://localhost:8080', () => {
|
|
assert.equal(constructModelsUrl('http://localhost:8080'), 'http://localhost:8080/v1/models');
|
|
});
|
|
|
|
it('should add /v1 when no version suffix: https://api.custom.com', () => {
|
|
assert.equal(constructModelsUrl('https://api.custom.com'), 'https://api.custom.com/v1/models');
|
|
});
|
|
|
|
it('should NOT produce double slashes in any case', () => {
|
|
const testCases = [
|
|
'https://api.openai.com/v1/',
|
|
'https://api.openai.com/v1//',
|
|
'https://api.anthropic.com/v1/',
|
|
'http://localhost:8080/',
|
|
];
|
|
|
|
for (const url of testCases) {
|
|
const result = constructModelsUrl(url);
|
|
assert.ok(!result.includes('//models'), `Double slash found in: ${result} (from: ${url})`);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Anthropic URL construction', () => {
|
|
const constructAnthropicUrl = (apiBase: string) => {
|
|
const normalized = apiBase.replace(/\/+$/, '');
|
|
return `${normalized}/models`;
|
|
};
|
|
|
|
it('should construct correct Anthropic URL without trailing slash', () => {
|
|
assert.equal(constructAnthropicUrl('https://api.anthropic.com/v1'), 'https://api.anthropic.com/v1/models');
|
|
});
|
|
|
|
it('should construct correct Anthropic URL WITH trailing slash', () => {
|
|
assert.equal(constructAnthropicUrl('https://api.anthropic.com/v1/'), 'https://api.anthropic.com/v1/models');
|
|
});
|
|
|
|
it('should NOT produce double slashes', () => {
|
|
const result = constructAnthropicUrl('https://api.anthropic.com/v1//');
|
|
assert.ok(!result.includes('//models'), `Double slash found in: ${result}`);
|
|
});
|
|
});
|
|
});
|