test(cliproxy): cover Claude long-context parity flows

- add regression coverage for runtime, api create flags, and help text

- add provider-editor tests for multi-key [1m] state and preset preservation

Refs #789
This commit is contained in:
Tam Nhu Tran
2026-03-26 16:36:14 -04:00
parent c05189b4b1
commit 5e11bba514
5 changed files with 305 additions and 2 deletions
@@ -136,6 +136,22 @@ describe('applyExtendedContextConfig', () => {
expect(env.ANTHROPIC_MODEL).toBe('claude-opus-4-5-20251101[1m]');
});
it('applies explicit Claude [1m] per tier and leaves unsupported Haiku plain', () => {
const env: NodeJS.ProcessEnv = {
ANTHROPIC_MODEL: 'claude-haiku-4-5-20251001',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-20251101',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-20250929',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
};
applyExtendedContextConfig(env, 'claude', true);
expect(env.ANTHROPIC_MODEL).toBe('claude-haiku-4-5-20251001');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-5-20251101[1m]');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-5-20250929[1m]');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5-20251001');
});
it('strips existing suffixes before catalog lookup', () => {
const env: NodeJS.ProcessEnv = {
ANTHROPIC_MODEL: 'gemini-2.5-pro(high)',
@@ -169,6 +185,20 @@ describe('applyExtendedContextConfig', () => {
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
});
it('strips stale Claude [1m] from unsupported mappings while preserving compatible ones', () => {
const env: NodeJS.ProcessEnv = {
ANTHROPIC_MODEL: 'claude-haiku-4-5-20251001[1m]',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-20251101[1m]',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-20250929[1m]',
};
applyExtendedContextConfig(env, 'claude', true);
expect(env.ANTHROPIC_MODEL).toBe('claude-haiku-4-5-20251001');
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-5-20251101[1m]');
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-5-20250929[1m]');
});
it('strips [1m] suffix when --no-1m is explicit even if model has it', () => {
const env: NodeJS.ProcessEnv = {
ANTHROPIC_MODEL: 'gemini-2.5-pro[1m]',
@@ -1,7 +1,10 @@
import { describe, expect, test } from 'bun:test';
import {
applyClaudeExtendedContextPreference,
collectUnexpectedApiArgs,
hasClaudeModelMapping,
hasExplicitClaudeExtendedContext,
parseApiCommandArgs,
} from '../../../src/commands/api-command/shared';
@@ -132,6 +135,27 @@ describe('api-command arg parser', () => {
expect(parsed.model).toBe('-preview');
expect(parsed.errors).toEqual([]);
});
test('parses --1m for explicit Claude long context', () => {
const parsed = parseApiCommandArgs(['my-api', '--1m']);
expect(parsed.extendedContext).toBe(true);
expect(parsed.errors).toEqual([]);
});
test('parses --no-1m for explicit standard-context preference', () => {
const parsed = parseApiCommandArgs(['my-api', '--no-1m']);
expect(parsed.extendedContext).toBe(false);
expect(parsed.errors).toEqual([]);
});
test('rejects conflicting --1m and --no-1m flags', () => {
const parsed = parseApiCommandArgs(['my-api', '--1m', '--no-1m']);
expect(parsed.extendedContext).toBeUndefined();
expect(parsed.errors).toEqual(['Cannot combine --1m and --no-1m']);
});
});
describe('collectUnexpectedApiArgs', () => {
@@ -155,3 +179,55 @@ describe('collectUnexpectedApiArgs', () => {
expect(parsed.errors).toEqual(['Unknown option: --bogus', 'Unexpected arguments: value']);
});
});
describe('Claude long-context mapping helpers', () => {
test('detects Claude mappings and explicit [1m] state across all tiers', () => {
const models = {
default: 'gpt-5.4',
opus: 'claude-opus-4-6[1m]',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
};
expect(hasClaudeModelMapping(models)).toBe(true);
expect(hasExplicitClaudeExtendedContext(models)).toBe(true);
});
test('applies [1m] only to compatible Claude mappings and leaves Haiku plain', () => {
const models = applyClaudeExtendedContextPreference(
{
default: 'claude-sonnet-4-6',
opus: 'claude-opus-4-6',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
},
true
);
expect(models).toEqual({
default: 'claude-sonnet-4-6[1m]',
opus: 'claude-opus-4-6[1m]',
sonnet: 'claude-sonnet-4-6[1m]',
haiku: 'claude-haiku-4-5-20251001',
});
});
test('strips [1m] from Claude mappings when standard context is requested', () => {
const models = applyClaudeExtendedContextPreference(
{
default: 'claude-sonnet-4-6[1m]',
opus: 'claude-opus-4-6[1m]',
sonnet: 'claude-sonnet-4-6[1m]',
haiku: 'claude-haiku-4-5-20251001',
},
false
);
expect(models).toEqual({
default: 'claude-sonnet-4-6',
opus: 'claude-opus-4-6',
sonnet: 'claude-sonnet-4-6',
haiku: 'claude-haiku-4-5-20251001',
});
});
});
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { showApiCommandHelp } from '../../../src/commands/api-command/help';
import { handleHelpCommand } from '../../../src/commands/help-command';
function stripAnsi(input: string): string {
@@ -103,4 +104,41 @@ describe('help command parity', () => {
true
);
});
test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => {
const lines: string[] = [];
console.log = (...args: unknown[]) => {
lines.push(args.map((arg) => String(arg)).join(' '));
};
await handleHelpCommand();
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]')).toBe(
true
);
expect(rendered.includes('CCS only controls the saved [1m] suffix.')).toBe(true);
expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(
true
);
});
test('api help documents create-time Claude [1m] flags and entitlement warning', async () => {
const lines: string[] = [];
console.log = (...args: unknown[]) => {
lines.push(args.map((arg) => String(arg)).join(' '));
};
await showApiCommandHelp();
const rendered = stripAnsi(lines.join('\n'));
expect(rendered.includes('--1m / --no-1m')).toBe(true);
expect(rendered.includes('ccs api create --preset anthropic --1m')).toBe(true);
expect(rendered.includes('Plain Claude model IDs stay on standard context by default.')).toBe(
true
);
expect(rendered.includes('some accounts can still return 429 for long-context requests')).toBe(
true
);
});
});
@@ -26,6 +26,7 @@ describe('ModelConfigSection presets', () => {
haikuModel="gpt-5-codex-mini"
providerModels={[]}
provider="codex"
onExtendedContextToggle={vi.fn()}
onApplyPreset={onApplyPreset}
onUpdateEnvValue={vi.fn()}
onOpenCustomPreset={vi.fn()}
@@ -55,10 +56,11 @@ describe('ModelConfigSection presets', () => {
savedPresets={[]}
currentModel="claude-opus-4-6-thinking"
opusModel="claude-opus-4-6-thinking"
sonnetModel="claude-sonnet-4-6"
haikuModel="claude-sonnet-4-6"
sonnetModel="gemini-3-pro-preview"
haikuModel="gemini-3-flash-preview"
providerModels={[]}
provider="agy"
onExtendedContextToggle={vi.fn()}
onApplyPreset={vi.fn()}
onUpdateEnvValue={vi.fn()}
onOpenCustomPreset={vi.fn()}
@@ -69,5 +71,6 @@ describe('ModelConfigSection presets', () => {
expect(screen.queryByText('Free Tier')).not.toBeInTheDocument();
expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Claude Opus 4.6 Thinking' })).toBeInTheDocument();
expect(screen.getByTestId('extended-context-toggle')).toBeInTheDocument();
});
});
@@ -0,0 +1,156 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import type { ReactNode } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AllProviders } from '../../../../setup/test-utils';
import { useProviderEditor } from '@/components/cliproxy/provider-editor/use-provider-editor';
function createJsonResponse(body: Record<string, unknown>, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
const wrapper = ({ children }: { children: ReactNode }) => (
<AllProviders>{children}</AllProviders>
);
describe('useProviderEditor', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('derives extended-context state from all Anthropic model env keys', async () => {
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/api/settings/claude/raw')) {
return Promise.resolve(
createJsonResponse({
profile: 'claude',
settings: {
env: {
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6[1m]',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
},
},
mtime: 1,
path: '~/.ccs/profiles/claude/settings.json',
})
);
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
})
);
const { result } = renderHook(() => useProviderEditor('claude'), { wrapper });
await waitFor(() => expect(result.current.currentModel).toBe('claude-sonnet-4-6'));
expect(result.current.extendedContextEnabled).toBe(true);
});
it('applies [1m] across compatible Claude mappings and leaves Haiku plain', async () => {
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/api/settings/claude/raw')) {
return Promise.resolve(
createJsonResponse({
profile: 'claude',
settings: {
env: {
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
},
},
mtime: 1,
path: '~/.ccs/profiles/claude/settings.json',
})
);
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
})
);
const { result } = renderHook(() => useProviderEditor('claude'), { wrapper });
await waitFor(() => expect(result.current.currentModel).toBe('claude-sonnet-4-6'));
act(() => {
result.current.toggleExtendedContext(true);
});
const nextSettings = JSON.parse(result.current.rawJsonContent);
expect(nextSettings.env).toMatchObject({
ANTHROPIC_MODEL: 'claude-sonnet-4-6[1m]',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6[1m]',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6[1m]',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
});
});
it('preserves explicit long-context intent when preset-style updates replace mappings', async () => {
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/api/settings/claude/raw')) {
return Promise.resolve(
createJsonResponse({
profile: 'claude',
settings: {
env: {
ANTHROPIC_MODEL: 'claude-sonnet-4-6[1m]',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6[1m]',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6[1m]',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
},
},
mtime: 1,
path: '~/.ccs/profiles/claude/settings.json',
})
);
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
})
);
const { result } = renderHook(() => useProviderEditor('claude'), { wrapper });
await waitFor(() => expect(result.current.extendedContextEnabled).toBe(true));
act(() => {
result.current.updateEnvValues({
ANTHROPIC_MODEL: 'claude-opus-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
});
});
const nextSettings = JSON.parse(result.current.rawJsonContent);
expect(nextSettings.env).toMatchObject({
ANTHROPIC_MODEL: 'claude-opus-4-6[1m]',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6[1m]',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6[1m]',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
});
});
});