From 2bd3c40c7ad4072634b395c0c5cfa7a29a2657a2 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 25 Feb 2026 17:00:49 +0700 Subject: [PATCH] fix(targets): sanitize invalid persisted target values - fallback unknown persisted targets to claude in profile listing and runtime resolution - add regression test for invalid profile target fallback --- src/api/services/profile-reader.ts | 21 +++++++++++++++------ src/targets/target-resolver.ts | 10 +++++++--- tests/unit/targets/target-resolver.test.ts | 5 +++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/api/services/profile-reader.ts b/src/api/services/profile-reader.ts index 028931c7..190426da 100644 --- a/src/api/services/profile-reader.ts +++ b/src/api/services/profile-reader.ts @@ -12,6 +12,15 @@ import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-c import type { TargetType } from '../../targets/target-adapter'; import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types'; +const VALID_TARGETS: ReadonlySet = new Set(['claude', 'droid']); + +function sanitizeTarget(target: unknown): TargetType { + if (typeof target === 'string' && VALID_TARGETS.has(target as TargetType)) { + return target as TargetType; + } + return 'claude'; +} + /** * Check if API profile exists in config */ @@ -69,7 +78,7 @@ export function listApiProfiles(): ApiListResult { settingsPath: profile.settings || 'config.yaml', isConfigured: isApiProfileConfigured(name), configSource: 'unified', - target: profile.target || 'claude', + target: sanitizeTarget(profile.target), }); } // CLIProxy variants @@ -82,12 +91,12 @@ export function listApiProfiles(): ApiListResult { name, provider, settings: variant?.settings || '-', - target: variant?.target || 'claude', + target: sanitizeTarget(variant?.target), }); } } else { const config = loadConfigSafe(); - const legacyTargetMap = (config as { profile_targets?: Record }) + const legacyTargetMap = (config as { profile_targets?: Record }) .profile_targets; for (const [name, settingsPath] of Object.entries(config.profiles)) { // Skip 'default' profile - it's the user's native Claude settings @@ -99,18 +108,18 @@ export function listApiProfiles(): ApiListResult { settingsPath: settingsPath as string, isConfigured: isApiProfileConfigured(name), configSource: 'legacy', - target: legacyTargetMap?.[name] || 'claude', + target: sanitizeTarget(legacyTargetMap?.[name]), }); } // CLIProxy variants if (config.cliproxy) { for (const [name, v] of Object.entries(config.cliproxy)) { - const variant = v as { provider: string; settings: string; target?: TargetType }; + const variant = v as { provider: string; settings: string; target?: unknown }; variants.push({ name, provider: variant.provider, settings: variant.settings, - target: variant.target || 'claude', + target: sanitizeTarget(variant.target), }); } } diff --git a/src/targets/target-resolver.ts b/src/targets/target-resolver.ts index edc23481..b7a811f4 100644 --- a/src/targets/target-resolver.ts +++ b/src/targets/target-resolver.ts @@ -48,9 +48,13 @@ interface ParsedTargetFlags { cleanedArgs: string[]; } +function isValidTarget(target: unknown): target is TargetType { + return typeof target === 'string' && VALID_TARGETS.has(target as TargetType); +} + function normalizeTargetValue(value: string): TargetType { const normalized = value.toLowerCase(); - if (VALID_TARGETS.has(normalized)) { + if (isValidTarget(normalized)) { return normalized as TargetType; } @@ -120,8 +124,8 @@ export function resolveTargetType( } // 2. Check per-profile config - if (profileConfig?.target) { - return profileConfig.target; + if (profileConfig?.target !== undefined) { + return isValidTarget(profileConfig.target) ? profileConfig.target : 'claude'; } // 3. Check argv[0] (busybox pattern) diff --git a/tests/unit/targets/target-resolver.test.ts b/tests/unit/targets/target-resolver.test.ts index a4082cdf..9841b9ce 100644 --- a/tests/unit/targets/target-resolver.test.ts +++ b/tests/unit/targets/target-resolver.test.ts @@ -37,6 +37,11 @@ describe('resolveTargetType', () => { expect(resolveTargetType([], { target: 'droid' })).toBe('droid'); }); + it('should fallback to claude when persisted profile target is invalid', () => { + process.argv = ['node', 'ccs']; + expect(resolveTargetType([], { target: 'invalid-target' as never })).toBe('claude'); + }); + it('should prioritize --target flag over profile config', () => { process.argv = ['node', 'ccs']; expect(resolveTargetType(['--target', 'claude'], { target: 'droid' })).toBe('claude');