mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 12:21:20 +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]');
|
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', () => {
|
it('strips existing suffixes before catalog lookup', () => {
|
||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
ANTHROPIC_MODEL: 'gemini-2.5-pro(high)',
|
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');
|
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', () => {
|
it('strips [1m] suffix when --no-1m is explicit even if model has it', () => {
|
||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
ANTHROPIC_MODEL: 'gemini-2.5-pro[1m]',
|
ANTHROPIC_MODEL: 'gemini-2.5-pro[1m]',
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { describe, expect, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
applyClaudeExtendedContextPreference,
|
||||||
collectUnexpectedApiArgs,
|
collectUnexpectedApiArgs,
|
||||||
|
hasClaudeModelMapping,
|
||||||
|
hasExplicitClaudeExtendedContext,
|
||||||
parseApiCommandArgs,
|
parseApiCommandArgs,
|
||||||
} from '../../../src/commands/api-command/shared';
|
} from '../../../src/commands/api-command/shared';
|
||||||
|
|
||||||
@@ -132,6 +135,27 @@ describe('api-command arg parser', () => {
|
|||||||
expect(parsed.model).toBe('-preview');
|
expect(parsed.model).toBe('-preview');
|
||||||
expect(parsed.errors).toEqual([]);
|
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', () => {
|
describe('collectUnexpectedApiArgs', () => {
|
||||||
@@ -155,3 +179,55 @@ describe('collectUnexpectedApiArgs', () => {
|
|||||||
expect(parsed.errors).toEqual(['Unknown option: --bogus', 'Unexpected arguments: value']);
|
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 { afterEach, describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { showApiCommandHelp } from '../../../src/commands/api-command/help';
|
||||||
import { handleHelpCommand } from '../../../src/commands/help-command';
|
import { handleHelpCommand } from '../../../src/commands/help-command';
|
||||||
|
|
||||||
function stripAnsi(input: string): string {
|
function stripAnsi(input: string): string {
|
||||||
@@ -103,4 +104,41 @@ describe('help command parity', () => {
|
|||||||
true
|
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"
|
haikuModel="gpt-5-codex-mini"
|
||||||
providerModels={[]}
|
providerModels={[]}
|
||||||
provider="codex"
|
provider="codex"
|
||||||
|
onExtendedContextToggle={vi.fn()}
|
||||||
onApplyPreset={onApplyPreset}
|
onApplyPreset={onApplyPreset}
|
||||||
onUpdateEnvValue={vi.fn()}
|
onUpdateEnvValue={vi.fn()}
|
||||||
onOpenCustomPreset={vi.fn()}
|
onOpenCustomPreset={vi.fn()}
|
||||||
@@ -55,10 +56,11 @@ describe('ModelConfigSection presets', () => {
|
|||||||
savedPresets={[]}
|
savedPresets={[]}
|
||||||
currentModel="claude-opus-4-6-thinking"
|
currentModel="claude-opus-4-6-thinking"
|
||||||
opusModel="claude-opus-4-6-thinking"
|
opusModel="claude-opus-4-6-thinking"
|
||||||
sonnetModel="claude-sonnet-4-6"
|
sonnetModel="gemini-3-pro-preview"
|
||||||
haikuModel="claude-sonnet-4-6"
|
haikuModel="gemini-3-flash-preview"
|
||||||
providerModels={[]}
|
providerModels={[]}
|
||||||
provider="agy"
|
provider="agy"
|
||||||
|
onExtendedContextToggle={vi.fn()}
|
||||||
onApplyPreset={vi.fn()}
|
onApplyPreset={vi.fn()}
|
||||||
onUpdateEnvValue={vi.fn()}
|
onUpdateEnvValue={vi.fn()}
|
||||||
onOpenCustomPreset={vi.fn()}
|
onOpenCustomPreset={vi.fn()}
|
||||||
@@ -69,5 +71,6 @@ describe('ModelConfigSection presets', () => {
|
|||||||
expect(screen.queryByText('Free Tier')).not.toBeInTheDocument();
|
expect(screen.queryByText('Free Tier')).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument();
|
expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: 'Claude Opus 4.6 Thinking' })).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