From 52ac29e7a8a6f7450eb3c1daa357d84d0b03031f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Fri, 10 Apr 2026 17:44:25 -0400 Subject: [PATCH] fix(api): constrain preset target defaults - limit preset default targets to persisted API profile targets - add regression coverage for preset target resolution and profile target persistence --- src/commands/api-command/create-command.ts | 25 +++++++--- src/shared/provider-preset-catalog.ts | 2 +- .../unit/api/profile-writer-anthropic.test.ts | 46 +++++++++++++++++++ .../api-create-target-resolution.test.ts | 17 +++++++ 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/unit/commands/api-create-target-resolution.test.ts diff --git a/src/commands/api-command/create-command.ts b/src/commands/api-command/create-command.ts index 063acf55..1909c2dc 100644 --- a/src/commands/api-command/create-command.ts +++ b/src/commands/api-command/create-command.ts @@ -269,12 +269,12 @@ async function resolveDefaultTarget( providedTarget: TargetType | undefined, yes: boolean | undefined ): Promise { - if (providedTarget) { - return providedTarget; - } - if (preset?.defaultTarget) { - console.log(info(`Using preset default target: ${preset.defaultTarget}`)); - return preset.defaultTarget; + const resolvedTarget = resolvePresetDefaultTarget(preset, providedTarget); + if (resolvedTarget) { + if (preset?.defaultTarget && !providedTarget) { + console.log(info(`Using preset default target: ${preset.defaultTarget}`)); + } + return resolvedTarget; } if (yes) { return 'claude'; @@ -287,6 +287,19 @@ async function resolveDefaultTarget( return useDroidByDefault ? 'droid' : 'claude'; } +export function resolvePresetDefaultTarget( + preset: Pick | null, + providedTarget: TargetType | undefined +): TargetType | null { + if (providedTarget) { + return providedTarget; + } + if (preset?.defaultTarget) { + return preset.defaultTarget; + } + return null; +} + async function resolveClaudeLongContextPreference( models: ModelMapping, explicitPreference: boolean | undefined, diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index fa39b1e8..b94fe6e8 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -6,7 +6,7 @@ */ export type PresetCategory = 'recommended' | 'alternative'; -export type ProviderPresetTarget = 'claude' | 'droid' | 'codex'; +export type ProviderPresetTarget = 'claude' | 'droid'; export const PROVIDER_PRESET_IDS = [ 'openrouter', diff --git a/tests/unit/api/profile-writer-anthropic.test.ts b/tests/unit/api/profile-writer-anthropic.test.ts index 6eab981b..0416c321 100644 --- a/tests/unit/api/profile-writer-anthropic.test.ts +++ b/tests/unit/api/profile-writer-anthropic.test.ts @@ -85,6 +85,52 @@ describe('profile-writer Anthropic direct', () => { expect(settings.env.ANTHROPIC_API_KEY).toBeUndefined(); }); + it('persists droid as the saved target for generic API profiles', () => { + const result = createApiProfile( + 'hf-target', + 'https://router.huggingface.co/v1', + 'hf_testkey123', + { + default: 'openai/gpt-oss-120b:fastest', + opus: 'openai/gpt-oss-120b:fastest', + sonnet: 'openai/gpt-oss-120b:fastest', + haiku: 'openai/gpt-oss-120b:fastest', + }, + 'droid' + ); + + expect(result.success).toBe(true); + + const configPath = path.join(tempHome, '.ccs', 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + expect(config.profiles['hf-target']).toBe('~/.ccs/hf-target.settings.json'); + expect(config.profile_targets['hf-target']).toBe('droid'); + }); + + it('does not persist a non-default target entry when the target is claude', () => { + const result = createApiProfile( + 'hf-target-claude', + 'https://router.huggingface.co/v1', + 'hf_testkey123', + { + default: 'openai/gpt-oss-120b:fastest', + opus: 'openai/gpt-oss-120b:fastest', + sonnet: 'openai/gpt-oss-120b:fastest', + haiku: 'openai/gpt-oss-120b:fastest', + }, + 'claude' + ); + + expect(result.success).toBe(true); + + const configPath = path.join(tempHome, '.ccs', 'config.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + expect(config.profiles['hf-target-claude']).toBe('~/.ccs/hf-target-claude.settings.json'); + expect(config.profile_targets?.['hf-target-claude']).toBeUndefined(); + }); + it('preserves OpenRouter ANTHROPIC_API_KEY blank behavior', () => { const result = createApiProfile( 'openrouter-test', diff --git a/tests/unit/commands/api-create-target-resolution.test.ts b/tests/unit/commands/api-create-target-resolution.test.ts new file mode 100644 index 00000000..eab0437f --- /dev/null +++ b/tests/unit/commands/api-create-target-resolution.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'bun:test'; + +import { resolvePresetDefaultTarget } from '../../../src/commands/api-command/create-command'; + +describe('api create target resolution', () => { + it('uses the preset default target when no explicit target is provided', () => { + expect(resolvePresetDefaultTarget({ defaultTarget: 'droid' }, undefined)).toBe('droid'); + }); + + it('lets an explicit target override the preset default target', () => { + expect(resolvePresetDefaultTarget({ defaultTarget: 'droid' }, 'claude')).toBe('claude'); + }); + + it('returns null when neither an explicit target nor a preset default exists', () => { + expect(resolvePresetDefaultTarget(null, undefined)).toBeNull(); + }); +});