mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
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:
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user