mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
fix(profile): close km legacy kimi compatibility gaps
This commit is contained in:
@@ -5,6 +5,8 @@
|
|||||||
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
|
* Mirrors the UI presets in ui/src/lib/provider-presets.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { resolveAliasToCanonical } from '../../utils/profile-compat';
|
||||||
|
|
||||||
export type PresetCategory = 'recommended' | 'alternative';
|
export type PresetCategory = 'recommended' | 'alternative';
|
||||||
|
|
||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
@@ -26,9 +28,6 @@ export interface ProviderPreset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api';
|
||||||
const LEGACY_PRESET_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
|
||||||
kimi: 'km',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider presets available via CLI and UI
|
* Provider presets available via CLI and UI
|
||||||
@@ -172,8 +171,8 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
|
|
||||||
/** Get preset by ID */
|
/** Get preset by ID */
|
||||||
export function getPresetById(id: string): ProviderPreset | undefined {
|
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||||
const normalized = id.toLowerCase();
|
const normalized = id.trim().toLowerCase();
|
||||||
const canonical = LEGACY_PRESET_ALIASES[normalized] || normalized;
|
const canonical = resolveAliasToCanonical(normalized).toLowerCase();
|
||||||
return PROVIDER_PRESETS.find((p) => p.id === canonical);
|
return PROVIDER_PRESETS.find((p) => p.id === canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
import { expandPath } from '../utils/helpers';
|
import { expandPath } from '../utils/helpers';
|
||||||
|
import { resolveAliasToCanonical } from '../utils/profile-compat';
|
||||||
import type { ProfileConfig, AccountConfig, CLIProxyVariantConfig } from './unified-config-types';
|
import type { ProfileConfig, AccountConfig, CLIProxyVariantConfig } from './unified-config-types';
|
||||||
import { createEmptyUnifiedConfig } from './unified-config-types';
|
import { createEmptyUnifiedConfig } from './unified-config-types';
|
||||||
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
||||||
@@ -23,9 +24,6 @@ import { saveUnifiedConfig, hasUnifiedConfig, loadUnifiedConfig } from './unifie
|
|||||||
import { infoBox, warn } from '../utils/ui';
|
import { infoBox, warn } from '../utils/ui';
|
||||||
|
|
||||||
const BACKUP_DIR_PREFIX = 'backup-v1-';
|
const BACKUP_DIR_PREFIX = 'backup-v1-';
|
||||||
const LEGACY_API_PROFILE_RENAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
||||||
kimi: 'km',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration result with details about what was migrated.
|
* Migration result with details about what was migrated.
|
||||||
@@ -76,7 +74,8 @@ export function loadMigrationCheckData(): MigrationCheckData {
|
|||||||
if (legacyConfig?.profiles && typeof legacyConfig.profiles === 'object' && unifiedConfig) {
|
if (legacyConfig?.profiles && typeof legacyConfig.profiles === 'object' && unifiedConfig) {
|
||||||
const legacyProfiles = legacyConfig.profiles as Record<string, unknown>;
|
const legacyProfiles = legacyConfig.profiles as Record<string, unknown>;
|
||||||
for (const profileName of Object.keys(legacyProfiles)) {
|
for (const profileName of Object.keys(legacyProfiles)) {
|
||||||
if (!unifiedConfig.profiles[profileName]) {
|
const targetProfileName = resolveAliasToCanonical(profileName);
|
||||||
|
if (!unifiedConfig.profiles[targetProfileName]) {
|
||||||
needsMigration = true;
|
needsMigration = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -185,13 +184,30 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
|
|||||||
// config.yaml only stores reference to the settings file
|
// config.yaml only stores reference to the settings file
|
||||||
if (oldConfig?.profiles) {
|
if (oldConfig?.profiles) {
|
||||||
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
||||||
const targetName = LEGACY_API_PROFILE_RENAMES[name] || name;
|
const sourceName = name.trim();
|
||||||
|
const targetName = resolveAliasToCanonical(sourceName);
|
||||||
const pathStr = settingsPath as string;
|
const pathStr = settingsPath as string;
|
||||||
const expandedPath = expandPath(pathStr);
|
const expandedPath = expandPath(pathStr);
|
||||||
|
|
||||||
|
const canonicalEntryValue = (oldConfig.profiles as Record<string, unknown>)[targetName];
|
||||||
|
const canonicalPathFromLegacyConfig =
|
||||||
|
sourceName !== targetName && typeof canonicalEntryValue === 'string'
|
||||||
|
? canonicalEntryValue
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Deterministic priority: explicit canonical profile wins over legacy alias rename.
|
||||||
|
if (canonicalPathFromLegacyConfig !== undefined) {
|
||||||
|
if (canonicalPathFromLegacyConfig !== pathStr) {
|
||||||
|
warnings.push(
|
||||||
|
`Skipped ${sourceName}: canonical profile "${targetName}" exists in config.json with different settings (${canonicalPathFromLegacyConfig})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Verify settings file exists
|
// Verify settings file exists
|
||||||
if (!fs.existsSync(expandedPath)) {
|
if (!fs.existsSync(expandedPath)) {
|
||||||
warnings.push(`Skipped ${name}: settings file not found at ${pathStr}`);
|
warnings.push(`Skipped ${sourceName}: settings file not found at ${pathStr}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +215,7 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
|
|||||||
const existing = unifiedConfig.profiles[targetName].settings;
|
const existing = unifiedConfig.profiles[targetName].settings;
|
||||||
if (existing !== pathStr) {
|
if (existing !== pathStr) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`Skipped ${name}: target profile "${targetName}" already exists with different settings (${existing})`
|
`Skipped ${sourceName}: target profile "${targetName}" already exists with different settings (${existing})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -212,11 +228,11 @@ export async function migrate(dryRun = false): Promise<MigrationResult> {
|
|||||||
};
|
};
|
||||||
unifiedConfig.profiles[targetName] = profile;
|
unifiedConfig.profiles[targetName] = profile;
|
||||||
migratedFiles.push(
|
migratedFiles.push(
|
||||||
`config.json.profiles.${name} → config.yaml.profiles.${targetName} (settings: ${pathStr})`
|
`config.json.profiles.${sourceName} → config.yaml.profiles.${targetName} (settings: ${pathStr})`
|
||||||
);
|
);
|
||||||
if (targetName !== name) {
|
if (targetName !== sourceName) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`Renamed legacy API profile "${name}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
`Renamed legacy API profile "${sourceName}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,16 +493,33 @@ async function migrateProfilesToUnified(
|
|||||||
|
|
||||||
// Migrate API profiles from config.json
|
// Migrate API profiles from config.json
|
||||||
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
for (const [name, settingsPath] of Object.entries(oldConfig.profiles)) {
|
||||||
const targetName = LEGACY_API_PROFILE_RENAMES[name] || name;
|
const sourceName = name.trim();
|
||||||
|
const targetName = resolveAliasToCanonical(sourceName);
|
||||||
const pathStr = settingsPath as string;
|
const pathStr = settingsPath as string;
|
||||||
|
|
||||||
|
const canonicalEntryValue = (oldConfig.profiles as Record<string, unknown>)[targetName];
|
||||||
|
const canonicalPathFromLegacyConfig =
|
||||||
|
sourceName !== targetName && typeof canonicalEntryValue === 'string'
|
||||||
|
? canonicalEntryValue
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Deterministic priority: explicit canonical profile wins over legacy alias rename.
|
||||||
|
if (canonicalPathFromLegacyConfig !== undefined) {
|
||||||
|
if (canonicalPathFromLegacyConfig !== pathStr) {
|
||||||
|
warnings.push(
|
||||||
|
`Skipped ${sourceName}: canonical profile "${targetName}" exists in config.json with different settings (${canonicalPathFromLegacyConfig})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// H7: Detect collision - profile exists in both configs
|
// H7: Detect collision - profile exists in both configs
|
||||||
if (unifiedConfig.profiles[targetName]) {
|
if (unifiedConfig.profiles[targetName]) {
|
||||||
// Check if settings differ (potential data loss)
|
// Check if settings differ (potential data loss)
|
||||||
const existingSettings = unifiedConfig.profiles[targetName].settings;
|
const existingSettings = unifiedConfig.profiles[targetName].settings;
|
||||||
if (existingSettings && existingSettings !== pathStr) {
|
if (existingSettings && existingSettings !== pathStr) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`Profile "${targetName}" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy ${name} (${pathStr})`
|
`Profile "${targetName}" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy ${sourceName} (${pathStr})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -496,7 +529,7 @@ async function migrateProfilesToUnified(
|
|||||||
|
|
||||||
// Verify settings file exists
|
// Verify settings file exists
|
||||||
if (!fs.existsSync(expandedPath)) {
|
if (!fs.existsSync(expandedPath)) {
|
||||||
warnings.push(`Skipped ${name}: settings file not found at ${pathStr}`);
|
warnings.push(`Skipped ${sourceName}: settings file not found at ${pathStr}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,9 +539,9 @@ async function migrateProfilesToUnified(
|
|||||||
settings: pathStr,
|
settings: pathStr,
|
||||||
};
|
};
|
||||||
migratedFiles.push(targetName);
|
migratedFiles.push(targetName);
|
||||||
if (targetName !== name) {
|
if (targetName !== sourceName) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
`Renamed legacy API profile "${name}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
`Renamed legacy API profile "${sourceName}" to "${targetName}" (ccs kimi API profile is now ccs km)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
modified = true;
|
modified = true;
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ const PROFILE_COMPAT_ALIASES: Readonly<Record<string, readonly string[]>> = Obje
|
|||||||
km: ['kimi'],
|
km: ['kimi'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a legacy alias to its canonical profile name.
|
||||||
|
* Returns trimmed input when no alias mapping exists.
|
||||||
|
*/
|
||||||
|
export function resolveAliasToCanonical(profileName: string): string {
|
||||||
|
const raw = profileName.trim();
|
||||||
|
const normalized = raw.toLowerCase();
|
||||||
|
|
||||||
|
for (const [canonical, aliases] of Object.entries(PROFILE_COMPAT_ALIASES)) {
|
||||||
|
if (aliases.includes(normalized)) {
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build lookup candidates for a profile.
|
* Build lookup candidates for a profile.
|
||||||
* Order: exact input -> lowercase form (if different) -> legacy aliases.
|
* Order: exact input -> lowercase form (if different) -> legacy aliases.
|
||||||
|
|||||||
@@ -87,24 +87,23 @@ export function checkConfigFile(): HealthCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check settings files (glm, kimi)
|
* Check settings files (glm, km with legacy kimi fallback)
|
||||||
*/
|
*/
|
||||||
export function checkSettingsFiles(ccsDir: string): HealthCheck[] {
|
export function checkSettingsFiles(ccsDir: string): HealthCheck[] {
|
||||||
const checks: HealthCheck[] = [];
|
const checks: HealthCheck[] = [];
|
||||||
const files = [
|
const profiles = ['glm', 'km'];
|
||||||
{ name: 'glm.settings.json', profile: 'glm' },
|
|
||||||
{ name: 'kimi.settings.json', profile: 'kimi' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const { DelegationValidator } = require('../../utils/delegation-validator');
|
const { DelegationValidator } = require('../../utils/delegation-validator');
|
||||||
|
|
||||||
for (const file of files) {
|
for (const profile of profiles) {
|
||||||
const filePath = path.join(ccsDir, file.name);
|
const fileName = `${profile}.settings.json`;
|
||||||
|
const filePath = path.join(ccsDir, fileName);
|
||||||
|
const validation = DelegationValidator.validate(profile);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!validation.valid && validation.error?.includes('Profile not found')) {
|
||||||
checks.push({
|
checks.push({
|
||||||
id: `settings-${file.profile}`,
|
id: `settings-${profile}`,
|
||||||
name: file.name,
|
name: fileName,
|
||||||
status: 'info',
|
status: 'info',
|
||||||
message: 'Not configured',
|
message: 'Not configured',
|
||||||
details: filePath,
|
details: filePath,
|
||||||
@@ -112,46 +111,50 @@ export function checkSettingsFiles(ccsDir: string): HealthCheck[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const resolvedPath = validation.settingsPath || filePath;
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
const resolvedName = path.basename(resolvedPath);
|
||||||
JSON.parse(content);
|
|
||||||
|
|
||||||
const validation = DelegationValidator.validate(file.profile);
|
if (validation.valid) {
|
||||||
|
|
||||||
if (validation.valid) {
|
|
||||||
checks.push({
|
|
||||||
id: `settings-${file.profile}`,
|
|
||||||
name: file.name,
|
|
||||||
status: 'ok',
|
|
||||||
message: 'Key configured',
|
|
||||||
details: filePath,
|
|
||||||
});
|
|
||||||
} else if (validation.error && validation.error.includes('placeholder')) {
|
|
||||||
checks.push({
|
|
||||||
id: `settings-${file.profile}`,
|
|
||||||
name: file.name,
|
|
||||||
status: 'warning',
|
|
||||||
message: 'Placeholder key',
|
|
||||||
details: filePath,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
checks.push({
|
|
||||||
id: `settings-${file.profile}`,
|
|
||||||
name: file.name,
|
|
||||||
status: 'ok',
|
|
||||||
message: 'Valid JSON',
|
|
||||||
details: filePath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
checks.push({
|
checks.push({
|
||||||
id: `settings-${file.profile}`,
|
id: `settings-${profile}`,
|
||||||
name: file.name,
|
name: resolvedName,
|
||||||
|
status: 'ok',
|
||||||
|
message: 'Key configured',
|
||||||
|
details: resolvedPath,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.error?.includes('placeholder')) {
|
||||||
|
checks.push({
|
||||||
|
id: `settings-${profile}`,
|
||||||
|
name: resolvedName,
|
||||||
|
status: 'warning',
|
||||||
|
message: 'Placeholder key',
|
||||||
|
details: resolvedPath,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.error?.includes('Failed to parse settings.json')) {
|
||||||
|
checks.push({
|
||||||
|
id: `settings-${profile}`,
|
||||||
|
name: resolvedName,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Invalid JSON',
|
message: 'Invalid JSON',
|
||||||
details: filePath,
|
details: resolvedPath,
|
||||||
});
|
});
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep prior behavior for non-placeholder validation issues (e.g., missing key).
|
||||||
|
checks.push({
|
||||||
|
id: `settings-${profile}`,
|
||||||
|
name: resolvedName,
|
||||||
|
status: 'ok',
|
||||||
|
message: 'Valid JSON',
|
||||||
|
details: resolvedPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return checks;
|
return checks;
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ describe('provider-presets', () => {
|
|||||||
expect(preset?.id).toBe('km');
|
expect(preset?.id).toBe('km');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves preset id with extra whitespace', () => {
|
||||||
|
const preset = getPresetById(' km ');
|
||||||
|
expect(preset?.id).toBe('km');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves uppercase legacy alias', () => {
|
||||||
|
const preset = getPresetById('KIMI');
|
||||||
|
expect(preset?.id).toBe('km');
|
||||||
|
});
|
||||||
|
|
||||||
it('treats legacy kimi alias as a valid preset id', () => {
|
it('treats legacy kimi alias as a valid preset id', () => {
|
||||||
expect(isValidPresetId('kimi')).toBe(true);
|
expect(isValidPresetId('kimi')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { loadMigrationCheckData, migrate } from '../../../src/config/migration-manager';
|
||||||
|
import { saveUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||||
|
import { createEmptyUnifiedConfig } from '../../../src/config/unified-config-types';
|
||||||
|
|
||||||
|
describe('migration-manager legacy kimi compatibility', () => {
|
||||||
|
let tempHome: string;
|
||||||
|
let ccsDir: string;
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-migration-manager-test-'));
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(tempHome)) {
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers explicit canonical km profile over legacy kimi when both exist', async () => {
|
||||||
|
const kmSettingsPath = path.join(ccsDir, 'km.settings.json');
|
||||||
|
const kimiSettingsPath = path.join(ccsDir, 'kimi.settings.json');
|
||||||
|
|
||||||
|
fs.writeFileSync(kmSettingsPath, JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: 'sk-km' } }));
|
||||||
|
fs.writeFileSync(
|
||||||
|
kimiSettingsPath,
|
||||||
|
JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: 'sk-kimi' } })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Intentionally place legacy alias first to verify deterministic behavior.
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
kimi: kimiSettingsPath,
|
||||||
|
km: kmSettingsPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await migrate(true);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(
|
||||||
|
result.migratedFiles.some((entry) =>
|
||||||
|
entry.includes(`config.json.profiles.km → config.yaml.profiles.km (settings: ${kmSettingsPath})`)
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
result.migratedFiles.some((entry) =>
|
||||||
|
entry.includes(`(settings: ${kimiSettingsPath})`)
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
result.warnings.some((warning) =>
|
||||||
|
warning.includes(
|
||||||
|
'Skipped kimi: canonical profile "km" exists in config.json with different settings'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renames case-variant legacy Kimi profile key to km', async () => {
|
||||||
|
const kimiSettingsPath = path.join(ccsDir, 'kimi.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
kimiSettingsPath,
|
||||||
|
JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: 'sk-kimi-case-variant' } })
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
Kimi: kimiSettingsPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await migrate(true);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(
|
||||||
|
result.migratedFiles.some((entry) =>
|
||||||
|
entry.includes('config.json.profiles.Kimi → config.yaml.profiles.km')
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats legacy kimi profile as migrated when unified config already has km', () => {
|
||||||
|
const unifiedConfig = createEmptyUnifiedConfig();
|
||||||
|
unifiedConfig.profiles.km = {
|
||||||
|
type: 'api',
|
||||||
|
settings: '~/.ccs/km.settings.json',
|
||||||
|
};
|
||||||
|
saveUnifiedConfig(unifiedConfig);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
kimi: '~/.ccs/kimi.settings.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkData = loadMigrationCheckData();
|
||||||
|
expect(checkData.needsMigration).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
import { describe, expect, it } from 'bun:test';
|
||||||
import { getProfileLookupCandidates, isLegacyProfileAlias } from '../../../src/utils/profile-compat';
|
import {
|
||||||
|
getProfileLookupCandidates,
|
||||||
|
isLegacyProfileAlias,
|
||||||
|
resolveAliasToCanonical,
|
||||||
|
} from '../../../src/utils/profile-compat';
|
||||||
|
|
||||||
describe('profile-compat', () => {
|
describe('profile-compat', () => {
|
||||||
describe('getProfileLookupCandidates', () => {
|
describe('getProfileLookupCandidates', () => {
|
||||||
@@ -14,6 +18,15 @@ describe('profile-compat', () => {
|
|||||||
it('normalizes uppercase input and still resolves aliases', () => {
|
it('normalizes uppercase input and still resolves aliases', () => {
|
||||||
expect(getProfileLookupCandidates('KM')).toEqual(['KM', 'km', 'kimi']);
|
expect(getProfileLookupCandidates('KM')).toEqual(['KM', 'km', 'kimi']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles surrounding whitespace', () => {
|
||||||
|
expect(getProfileLookupCandidates(' km ')).toEqual(['km', 'kimi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty candidates for empty input', () => {
|
||||||
|
expect(getProfileLookupCandidates('')).toEqual([]);
|
||||||
|
expect(getProfileLookupCandidates(' ')).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isLegacyProfileAlias', () => {
|
describe('isLegacyProfileAlias', () => {
|
||||||
@@ -30,4 +43,16 @@ describe('profile-compat', () => {
|
|||||||
expect(isLegacyProfileAlias('glm', 'kimi')).toBe(false);
|
expect(isLegacyProfileAlias('glm', 'kimi')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolveAliasToCanonical', () => {
|
||||||
|
it('maps legacy kimi alias to canonical km', () => {
|
||||||
|
expect(resolveAliasToCanonical('kimi')).toBe('km');
|
||||||
|
expect(resolveAliasToCanonical('KIMI')).toBe('km');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps canonical and non-aliased names', () => {
|
||||||
|
expect(resolveAliasToCanonical('km')).toBe('km');
|
||||||
|
expect(resolveAliasToCanonical('glm')).toBe('glm');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { checkSettingsFiles } from '../../../src/web-server/health/config-checks';
|
||||||
|
|
||||||
|
describe('web-server config-checks settings compatibility', () => {
|
||||||
|
let tempHome: string;
|
||||||
|
let ccsDir: string;
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-config-checks-test-'));
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalCcsHome !== undefined) {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
} else {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(tempHome)) {
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports km as configured when only legacy kimi.settings.json exists', () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'kimi.settings.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'sk-live-kimi-compat',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const checks = checkSettingsFiles(ccsDir);
|
||||||
|
const kmCheck = checks.find((check) => check.id === 'settings-km');
|
||||||
|
|
||||||
|
expect(kmCheck).toBeDefined();
|
||||||
|
expect(kmCheck?.status).toBe('ok');
|
||||||
|
expect(kmCheck?.name).toBe('kimi.settings.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -176,6 +176,10 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const LEGACY_PRESET_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
kimi: 'km',
|
||||||
|
});
|
||||||
|
|
||||||
/** Get presets by category */
|
/** Get presets by category */
|
||||||
export function getPresetsByCategory(category: PresetCategory): ProviderPreset[] {
|
export function getPresetsByCategory(category: PresetCategory): ProviderPreset[] {
|
||||||
return PROVIDER_PRESETS.filter((p) => p.category === category);
|
return PROVIDER_PRESETS.filter((p) => p.category === category);
|
||||||
@@ -183,7 +187,9 @@ export function getPresetsByCategory(category: PresetCategory): ProviderPreset[]
|
|||||||
|
|
||||||
/** Get preset by ID */
|
/** Get preset by ID */
|
||||||
export function getPresetById(id: string): ProviderPreset | undefined {
|
export function getPresetById(id: string): ProviderPreset | undefined {
|
||||||
return PROVIDER_PRESETS.find((p) => p.id.toLowerCase() === id.toLowerCase());
|
const normalized = id.trim().toLowerCase();
|
||||||
|
const canonical = LEGACY_PRESET_ALIASES[normalized] || normalized;
|
||||||
|
return PROVIDER_PRESETS.find((p) => p.id.toLowerCase() === canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a URL matches a known preset */
|
/** Check if a URL matches a known preset */
|
||||||
|
|||||||
Reference in New Issue
Block a user