mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 12:16:38 +00:00
Merge pull request #660 from kaitranntt/kai/feat/cliproxy-multi-provider-analytics
fix(agy): remove deprecated 4.5 selector options
This commit is contained in:
33 files changed
+914
-176
No files matched your search
@@ -0,0 +1,48 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { createApiProfile } from '../../../src/api/services/profile-writer';
|
||||
|
||||
describe('profile-writer AGY denylist', () => {
|
||||
let tempHome = '';
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-profile-writer-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome === undefined) {
|
||||
delete process.env.CCS_HOME;
|
||||
} else {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
}
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY 4.5 models on profile create', () => {
|
||||
const result = createApiProfile(
|
||||
'agy-denied',
|
||||
'http://127.0.0.1:8317/api/provider/agy',
|
||||
'test-token',
|
||||
{
|
||||
default: 'claude-opus-4.5',
|
||||
opus: 'claude-opus-4.5',
|
||||
sonnet: 'claude-sonnet-4.5',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
'claude'
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('denylist');
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'agy-denied.settings.json');
|
||||
expect(fs.existsSync(settingsPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
const validTier = {
|
||||
provider: 'agy',
|
||||
model: 'claude-sonnet-4-5-thinking',
|
||||
model: 'claude-sonnet-4-6',
|
||||
};
|
||||
|
||||
describe('validateCompositeDefaultTier', () => {
|
||||
@@ -77,4 +77,17 @@ describe('validateCompositeTiers', () => {
|
||||
|
||||
expect(error).toContain("Circular fallback in tier 'opus'");
|
||||
});
|
||||
|
||||
it('rejects denylisted agy 4.5 models', () => {
|
||||
const error = validateCompositeTiers(
|
||||
{
|
||||
opus: { provider: 'agy', model: 'claude-opus-4.5-thinking' },
|
||||
sonnet: { provider: 'gemini', model: 'gemini-2.5-pro' },
|
||||
haiku: { provider: 'gemini', model: 'gemini-2.5-flash' },
|
||||
},
|
||||
{ defaultTier: 'opus', requireAllTiers: true }
|
||||
);
|
||||
|
||||
expect(error).toContain('denylist');
|
||||
});
|
||||
});
|
||||
@@ -73,7 +73,7 @@ cliproxy:
|
||||
default_tier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: 'cliproxy/composite-test.settings.json',
|
||||
@@ -99,7 +99,7 @@ cliproxy:
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.variant?.tiers?.opus.model).toBe('claude-opus-4-6-thinking');
|
||||
expect(result.variant?.tiers?.sonnet.model).toBe('claude-sonnet-4-5-thinking'); // Unchanged
|
||||
expect(result.variant?.tiers?.sonnet.model).toBe('claude-sonnet-4-6'); // Unchanged
|
||||
expect(result.variant?.tiers?.haiku.model).toBe('claude-haiku-4-5-20251001'); // Unchanged
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ cliproxy:
|
||||
default_tier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: 'cliproxy/composite-test.settings.json',
|
||||
@@ -144,7 +144,7 @@ cliproxy:
|
||||
default_tier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: 'cliproxy/composite-test.settings.json',
|
||||
@@ -188,7 +188,7 @@ cliproxy:
|
||||
thinking: 'xhigh',
|
||||
account: 'team-a',
|
||||
},
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: 'cliproxy/composite-test.settings.json',
|
||||
@@ -228,7 +228,7 @@ cliproxy:
|
||||
default_tier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: customSettingsPath,
|
||||
@@ -244,9 +244,9 @@ cliproxy:
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8318',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-5-thinking',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-preview',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
|
||||
CUSTOM_ENV: 'preserve-this',
|
||||
},
|
||||
@@ -261,7 +261,7 @@ cliproxy:
|
||||
|
||||
const result = updateCompositeVariant('test', {
|
||||
tiers: {
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking(high)' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6(high)' },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -273,7 +273,7 @@ cliproxy:
|
||||
hooks: { PreToolUse: unknown[] };
|
||||
customPreset: boolean;
|
||||
};
|
||||
expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-5-thinking(high)');
|
||||
expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(high)');
|
||||
expect(updatedSettings.env.CUSTOM_ENV).toBe('preserve-this');
|
||||
expect(updatedSettings.hooks.PreToolUse.length).toBe(1);
|
||||
expect(updatedSettings.customPreset).toBe(true);
|
||||
@@ -377,7 +377,7 @@ cliproxy:
|
||||
defaultTier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
} as unknown as {
|
||||
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
|
||||
sonnet: { provider: 'gemini' | 'codex' | 'agy'; model: string };
|
||||
@@ -395,7 +395,7 @@ cliproxy:
|
||||
defaultTier: 'sonnet',
|
||||
tiers: {
|
||||
opus: null,
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
} as unknown as {
|
||||
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
|
||||
@@ -414,7 +414,7 @@ cliproxy:
|
||||
defaultTier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
});
|
||||
@@ -479,7 +479,7 @@ cliproxy:
|
||||
default_tier: 'sonnet',
|
||||
tiers: {
|
||||
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-5-thinking' },
|
||||
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
|
||||
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
|
||||
},
|
||||
settings: 'cliproxy/composite-test.settings.json',
|
||||
@@ -547,7 +547,7 @@ cliproxy:
|
||||
model: gemini-3-pro-preview
|
||||
sonnet:
|
||||
provider: agy
|
||||
model: claude-sonnet-4-5-thinking
|
||||
model: claude-sonnet-4-6
|
||||
haiku:
|
||||
provider: agy
|
||||
model: claude-haiku-4-5-20251001
|
||||
@@ -588,7 +588,7 @@ cliproxy:
|
||||
thinking: xhigh
|
||||
sonnet:
|
||||
provider: agy
|
||||
model: claude-sonnet-4-5-thinking
|
||||
model: claude-sonnet-4-6
|
||||
thinking: medium
|
||||
haiku:
|
||||
provider: agy
|
||||
@@ -632,7 +632,7 @@ cliproxy:
|
||||
model: claude-opus-4-6-thinking
|
||||
sonnet:
|
||||
provider: agy
|
||||
model: claude-sonnet-4-5-thinking
|
||||
model: claude-sonnet-4-6
|
||||
haiku:
|
||||
provider: agy
|
||||
model: claude-haiku-4-5-20251001
|
||||
@@ -671,7 +671,7 @@ cliproxy:
|
||||
model: claude-opus-4-6-thinking
|
||||
sonnet:
|
||||
provider: agy
|
||||
model: claude-sonnet-4-5-thinking
|
||||
model: claude-sonnet-4-6
|
||||
settings: cliproxy/bad.settings.json
|
||||
port: 8318
|
||||
good:
|
||||
|
||||
@@ -584,9 +584,16 @@ auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
const config = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
|
||||
// Claude aliases should have fork: true
|
||||
assert(config.includes('claude-sonnet-4-5'), 'Should include Claude sonnet model');
|
||||
assert(config.includes('claude-sonnet-4-6'), 'Should include Claude Sonnet 4.6 model');
|
||||
assert(config.includes('fork: true'), 'Should include fork: true for Claude aliases');
|
||||
assert(
|
||||
!config.includes('claude-sonnet-4-5'),
|
||||
'Should not include denylisted Claude Sonnet 4.5 aliases'
|
||||
);
|
||||
assert(
|
||||
!config.includes('claude-opus-4-5'),
|
||||
'Should not include denylisted Claude Opus 4.5 aliases'
|
||||
);
|
||||
assert(
|
||||
!config.includes('alias: gemini-claude-'),
|
||||
'Should not emit deprecated gemini-claude aliases'
|
||||
|
||||
@@ -118,6 +118,50 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('migrates denylisted AGY 4.5 model IDs to supported 4.6 fallbacks', () => {
|
||||
writeSettings(
|
||||
settingsPath,
|
||||
{
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'claude-opus-4.5',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.5-thinking',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-sonnet-4-5',
|
||||
},
|
||||
{
|
||||
presets: [
|
||||
{
|
||||
name: 'legacy-45',
|
||||
default: 'claude-opus-4.5',
|
||||
opus: 'claude-opus-4-5-thinking',
|
||||
sonnet: 'claude-sonnet-4.5-thinking',
|
||||
haiku: 'claude-sonnet-4-5',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const env = getEffectiveEnvVars('agy', 8317, settingsPath);
|
||||
expect(env.ANTHROPIC_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-6');
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
|
||||
env: Record<string, string>;
|
||||
presets?: Array<Record<string, string>>;
|
||||
};
|
||||
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets?.[0]?.default).toBe('claude-opus-4-6-thinking');
|
||||
expect(persisted.presets?.[0]?.opus).toBe('claude-opus-4-6-thinking');
|
||||
expect(persisted.presets?.[0]?.sonnet).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets?.[0]?.haiku).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('migrates codex preset model mappings to canonical IDs', () => {
|
||||
writeSettings(
|
||||
settingsPath,
|
||||
@@ -269,11 +313,11 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(8192)');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-5');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(repaired.presets?.[0]?.default).toBe('claude-sonnet-4-6');
|
||||
expect(repaired.presets?.[0]?.opus).toBe('claude-opus-4-6-thinking');
|
||||
expect(repaired.presets?.[0]?.sonnet).toBe('claude-sonnet-4-6');
|
||||
expect(repaired.presets?.[0]?.haiku).toBe('claude-sonnet-4-5');
|
||||
expect(repaired.presets?.[0]?.haiku).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('migrates codex effort-suffixed preset IDs during ensureProviderSettings', () => {
|
||||
|
||||
@@ -3,16 +3,28 @@ import { findModel, supportsThinking } from '../../../src/cliproxy/model-catalog
|
||||
|
||||
describe('model-catalog compatibility lookups', () => {
|
||||
it('finds agy Claude models using dotted major.minor IDs', () => {
|
||||
const dottedThinking = findModel('agy', 'claude-sonnet-4.5-thinking');
|
||||
const dottedNonThinking = findModel('agy', 'claude-sonnet-4.5');
|
||||
const dottedThinking = findModel('agy', 'claude-opus-4.6-thinking');
|
||||
const dottedNonThinking = findModel('agy', 'claude-sonnet-4.6');
|
||||
|
||||
expect(dottedThinking?.id).toBe('claude-sonnet-4-5-thinking');
|
||||
expect(dottedNonThinking?.id).toBe('claude-sonnet-4-5');
|
||||
expect(dottedThinking?.id).toBe('claude-opus-4-6-thinking');
|
||||
expect(dottedNonThinking?.id).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('maps legacy agy 4.5 IDs to canonical 4.6 catalog models', () => {
|
||||
const legacyOpusThinking = findModel('agy', 'claude-opus-4.5-thinking');
|
||||
const legacySonnetThinking = findModel('agy', 'claude-sonnet-4.5-thinking');
|
||||
const legacySonnet = findModel('agy', 'claude-sonnet-4.5');
|
||||
|
||||
expect(legacyOpusThinking?.id).toBe('claude-opus-4-6-thinking');
|
||||
expect(legacySonnetThinking?.id).toBe('claude-sonnet-4-6');
|
||||
expect(legacySonnet?.id).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('supports thinking checks for dotted agy model IDs', () => {
|
||||
expect(supportsThinking('agy', 'claude-opus-4.6-thinking')).toBe(true);
|
||||
expect(supportsThinking('agy', 'claude-sonnet-4.5')).toBe(false);
|
||||
expect(supportsThinking('agy', 'claude-sonnet-4.6')).toBe(true);
|
||||
expect(supportsThinking('agy', 'claude-opus-4.5-thinking')).toBe(true);
|
||||
expect(supportsThinking('agy', 'claude-sonnet-4.5')).toBe(true);
|
||||
});
|
||||
|
||||
it('maps legacy sonnet 4.6 thinking aliases to canonical agy model', () => {
|
||||
|
||||
@@ -69,20 +69,11 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(MODEL_CATALOG.agy.defaultModel, 'claude-opus-4-6-thinking');
|
||||
});
|
||||
|
||||
it('includes Claude Opus 4.5 Thinking', () => {
|
||||
it('includes Claude Opus 4.6 Thinking', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.5 Thinking');
|
||||
assert.strictEqual(opus.name, 'Claude Opus 4.5 Thinking');
|
||||
});
|
||||
|
||||
it('includes Claude Sonnet 4.5 Thinking', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const sonnetThinking = MODEL_CATALOG.agy.models.find(
|
||||
(m) => m.id === 'claude-sonnet-4-5-thinking'
|
||||
);
|
||||
assert(sonnetThinking, 'Should include Claude Sonnet 4.5 Thinking');
|
||||
assert.strictEqual(sonnetThinking.name, 'Claude Sonnet 4.5 Thinking');
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.6 Thinking');
|
||||
assert.strictEqual(opus.name, 'Claude Opus 4.6 Thinking');
|
||||
});
|
||||
|
||||
it('includes Claude Sonnet 4.6', () => {
|
||||
@@ -92,11 +83,12 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(sonnet.name, 'Claude Sonnet 4.6');
|
||||
});
|
||||
|
||||
it('includes Claude Sonnet 4.5', () => {
|
||||
it('does not include deprecated Claude 4.5 AGY models', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const sonnet = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-5');
|
||||
assert(sonnet, 'Should include Claude Sonnet 4.5');
|
||||
assert.strictEqual(sonnet.name, 'Claude Sonnet 4.5');
|
||||
const ids = MODEL_CATALOG.agy.models.map((m) => m.id);
|
||||
assert.strictEqual(ids.includes('claude-opus-4-5-thinking'), false);
|
||||
assert.strictEqual(ids.includes('claude-sonnet-4-5-thinking'), false);
|
||||
assert.strictEqual(ids.includes('claude-sonnet-4-5'), false);
|
||||
});
|
||||
|
||||
it('includes Gemini 3 Pro (free via Antigravity)', () => {
|
||||
@@ -108,9 +100,9 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(gem3.tier, undefined, 'AGY models should not have paid tier');
|
||||
});
|
||||
|
||||
it('has 6 models total', () => {
|
||||
it('has 3 models total', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 6);
|
||||
assert.strictEqual(MODEL_CATALOG.agy.models.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,11 +195,11 @@ describe('Model Catalog', () => {
|
||||
});
|
||||
|
||||
describe('findModel', () => {
|
||||
it('finds Claude Opus 4.5 Thinking in agy', () => {
|
||||
it('finds Claude Opus 4.6 Thinking in agy', () => {
|
||||
const { findModel } = modelCatalog;
|
||||
const model = findModel('agy', 'claude-opus-4-5-thinking');
|
||||
const model = findModel('agy', 'claude-opus-4-6-thinking');
|
||||
assert(model, 'Should find model');
|
||||
assert.strictEqual(model.name, 'Claude Opus 4.5 Thinking');
|
||||
assert.strictEqual(model.name, 'Claude Opus 4.6 Thinking');
|
||||
});
|
||||
|
||||
it('finds Gemini 2.5 Pro in gemini', () => {
|
||||
@@ -217,6 +209,16 @@ describe('Model Catalog', () => {
|
||||
assert.strictEqual(model.name, 'Gemini 2.5 Pro');
|
||||
});
|
||||
|
||||
it('maps legacy AGY 4.5 IDs to supported 4.6 models', () => {
|
||||
const { findModel } = modelCatalog;
|
||||
const legacyOpus = findModel('agy', 'claude-opus-4-5-thinking');
|
||||
const legacySonnetThinking = findModel('agy', 'claude-sonnet-4-5-thinking');
|
||||
const legacySonnet = findModel('agy', 'claude-sonnet-4-5');
|
||||
assert.strictEqual(legacyOpus?.id, 'claude-opus-4-6-thinking');
|
||||
assert.strictEqual(legacySonnetThinking?.id, 'claude-sonnet-4-6');
|
||||
assert.strictEqual(legacySonnet?.id, 'claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('returns undefined for unknown model', () => {
|
||||
const { findModel } = modelCatalog;
|
||||
const model = findModel('agy', 'unknown-model');
|
||||
@@ -269,19 +271,17 @@ describe('Model Catalog', () => {
|
||||
});
|
||||
|
||||
describe('Thinking models ordering', () => {
|
||||
it('Claude Opus 4.5 Thinking is not deprecated', () => {
|
||||
it('Claude Opus 4.6 Thinking is not deprecated', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.5 Thinking');
|
||||
const opus = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-opus-4-6-thinking');
|
||||
assert(opus, 'Should include Claude Opus 4.6 Thinking');
|
||||
assert.strictEqual(opus.deprecated, undefined, 'Should not be marked as deprecated');
|
||||
});
|
||||
|
||||
it('Claude Sonnet 4.5 Thinking is not deprecated', () => {
|
||||
it('Claude Sonnet 4.6 is not deprecated', () => {
|
||||
const { MODEL_CATALOG } = modelCatalog;
|
||||
const sonnetThinking = MODEL_CATALOG.agy.models.find(
|
||||
(m) => m.id === 'claude-sonnet-4-5-thinking'
|
||||
);
|
||||
assert(sonnetThinking, 'Should include Claude Sonnet 4.5 Thinking');
|
||||
const sonnetThinking = MODEL_CATALOG.agy.models.find((m) => m.id === 'claude-sonnet-4-6');
|
||||
assert(sonnetThinking, 'Should include Claude Sonnet 4.6');
|
||||
assert.strictEqual(
|
||||
sonnetThinking.deprecated,
|
||||
undefined,
|
||||
@@ -294,31 +294,27 @@ describe('Model Catalog', () => {
|
||||
const models = MODEL_CATALOG.agy.models;
|
||||
|
||||
// Find indices of thinking models
|
||||
const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-5-thinking');
|
||||
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5-thinking');
|
||||
const opusIdx = models.findIndex((m) => m.id === 'claude-opus-4-6-thinking');
|
||||
const sonnetThinkingIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-6');
|
||||
|
||||
// Find indices of non-thinking models
|
||||
const sonnetIdx = models.findIndex((m) => m.id === 'claude-sonnet-4-5');
|
||||
// Find indices of the remaining non-Claude model
|
||||
const geminiIdx = models.findIndex((m) => m.id === 'gemini-3-pro-preview');
|
||||
|
||||
// Thinking models should come before non-thinking models
|
||||
assert(opusIdx < sonnetIdx, 'Opus Thinking should be above non-thinking Sonnet');
|
||||
assert(opusIdx < geminiIdx, 'Opus Thinking should be above non-thinking Gemini');
|
||||
assert(sonnetThinkingIdx < sonnetIdx, 'Sonnet Thinking should be above non-thinking Sonnet');
|
||||
assert(sonnetThinkingIdx < geminiIdx, 'Sonnet Thinking should be above non-thinking Gemini');
|
||||
// Primary Claude choices should appear ahead of Gemini fallback.
|
||||
assert(opusIdx < geminiIdx, 'Opus should be above Gemini');
|
||||
assert(sonnetThinkingIdx < geminiIdx, 'Sonnet should be above Gemini');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isModelDeprecated', () => {
|
||||
it('returns false for thinking models (no longer deprecated)', () => {
|
||||
it('returns false for active AGY Claude models', () => {
|
||||
const { isModelDeprecated } = modelCatalog;
|
||||
assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-5-thinking'), false);
|
||||
assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-5-thinking'), false);
|
||||
assert.strictEqual(isModelDeprecated('agy', 'claude-opus-4-6-thinking'), false);
|
||||
assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-6'), false);
|
||||
});
|
||||
|
||||
it('returns false for non-deprecated models', () => {
|
||||
const { isModelDeprecated } = modelCatalog;
|
||||
assert.strictEqual(isModelDeprecated('agy', 'claude-sonnet-4-5'), false);
|
||||
assert.strictEqual(isModelDeprecated('agy', 'gemini-3-pro-preview'), false);
|
||||
});
|
||||
|
||||
@@ -329,15 +325,15 @@ describe('Model Catalog', () => {
|
||||
});
|
||||
|
||||
describe('getModelDeprecationReason', () => {
|
||||
it('returns undefined for thinking models (no longer deprecated)', () => {
|
||||
it('returns undefined for active AGY Claude models', () => {
|
||||
const { getModelDeprecationReason } = modelCatalog;
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-5-thinking'), undefined);
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-5-thinking'), undefined);
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-opus-4-6-thinking'), undefined);
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-6'), undefined);
|
||||
});
|
||||
|
||||
it('returns undefined for non-deprecated models', () => {
|
||||
const { getModelDeprecationReason } = modelCatalog;
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'claude-sonnet-4-5'), undefined);
|
||||
assert.strictEqual(getModelDeprecationReason('agy', 'gemini-3-pro-preview'), undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,10 @@ import { describe, it, expect } from 'bun:test';
|
||||
import {
|
||||
canonicalizeModelIdForProvider,
|
||||
extractProviderFromPathname,
|
||||
getDeniedModelIdReasonForProvider,
|
||||
isDeniedAntigravityModelId,
|
||||
isAntigravityProvider,
|
||||
migrateDeniedAntigravityModelAliases,
|
||||
normalizeClaudeDottedMajorMinor,
|
||||
normalizeClaudeDottedThinkingMajorMinor,
|
||||
normalizeModelIdForProvider,
|
||||
@@ -64,6 +67,13 @@ describe('model-id-normalizer', () => {
|
||||
expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'agy')).toBe(
|
||||
'claude-opus-4-6-thinking'
|
||||
);
|
||||
expect(normalizeModelIdForProvider('claude-opus-4.5-thinking', 'agy')).toBe(
|
||||
'claude-opus-4-5-thinking'
|
||||
);
|
||||
expect(normalizeModelIdForProvider('claude-sonnet-4.5-thinking', 'agy')).toBe(
|
||||
'claude-sonnet-4-5-thinking'
|
||||
);
|
||||
expect(normalizeModelIdForProvider('claude-sonnet-4.5', 'agy')).toBe('claude-sonnet-4-5');
|
||||
expect(normalizeModelIdForProvider('claude-opus-4.6-thinking', 'gemini')).toBe(
|
||||
'claude-opus-4.6-thinking'
|
||||
);
|
||||
@@ -98,23 +108,41 @@ describe('model-id-normalizer', () => {
|
||||
describe('env normalization', () => {
|
||||
it('normalizes model env vars for antigravity only', () => {
|
||||
const input: NodeJS.ProcessEnv = {
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.6-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.6',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4.5-thinking',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4.5-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.5',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5',
|
||||
UNRELATED: 'keep-me',
|
||||
};
|
||||
|
||||
const normalized = normalizeModelEnvVarsForProvider(input, 'agy');
|
||||
expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-6-thinking');
|
||||
expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(normalized.ANTHROPIC_MODEL).toBe('claude-sonnet-4-5-thinking');
|
||||
expect(normalized.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('claude-opus-4-5-thinking');
|
||||
expect(normalized.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('claude-sonnet-4-5');
|
||||
expect(normalized.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
|
||||
expect(normalized.UNRELATED).toBe('keep-me');
|
||||
|
||||
const unchanged = normalizeModelEnvVarsForProvider(input, 'gemini');
|
||||
expect(unchanged.ANTHROPIC_MODEL).toBe('claude-sonnet-4.6-thinking');
|
||||
expect(unchanged.ANTHROPIC_MODEL).toBe('claude-sonnet-4.5-thinking');
|
||||
expect(unchanged.UNRELATED).toBe('keep-me');
|
||||
});
|
||||
|
||||
it('flags denylisted antigravity models', () => {
|
||||
expect(isDeniedAntigravityModelId('claude-opus-4-5-thinking')).toBe(true);
|
||||
expect(isDeniedAntigravityModelId('claude-sonnet-4.5')).toBe(true);
|
||||
expect(getDeniedModelIdReasonForProvider('claude-sonnet-4.5', 'agy')).toContain('denylist');
|
||||
expect(getDeniedModelIdReasonForProvider('claude-sonnet-4.5', 'gemini')).toBeNull();
|
||||
expect(getDeniedModelIdReasonForProvider('claude-haiku-4.5', 'agy')).toBeNull();
|
||||
});
|
||||
|
||||
it('migrates denylisted antigravity IDs to supported 4.6 fallbacks', () => {
|
||||
expect(migrateDeniedAntigravityModelAliases('claude-opus-4.5')).toBe(
|
||||
'claude-opus-4-6-thinking'
|
||||
);
|
||||
expect(migrateDeniedAntigravityModelAliases('claude-sonnet-4-5-thinking(8192)')).toBe(
|
||||
'claude-sonnet-4-6(8192)'
|
||||
);
|
||||
expect(migrateDeniedAntigravityModelAliases('claude-haiku-4-5')).toBe('claude-haiku-4-5');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -253,6 +253,31 @@ describe('ToolSanitizationProxy Integration', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY 4.5 model IDs on antigravity provider route', async () => {
|
||||
const proxy = new ToolSanitizationProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
|
||||
});
|
||||
const port = await proxy.start();
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/provider/agy/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'claude-sonnet-4.5',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = (await response.json()) as { error: string };
|
||||
expect(body.error).toContain('denylist');
|
||||
expect(lastRequest).toBeNull();
|
||||
} finally {
|
||||
proxy.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not rewrite AGY legacy aliases on non-AGY provider routes', async () => {
|
||||
const proxy = new ToolSanitizationProxy({
|
||||
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
|
||||
|
||||
@@ -83,6 +83,15 @@ cliproxy:
|
||||
expect(result.error).toContain('Changing provider requires model update');
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY 4.5 model updates', () => {
|
||||
const result = updateVariant('demo', {
|
||||
provider: 'agy',
|
||||
model: 'claude-sonnet-4.5',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('denylist');
|
||||
});
|
||||
|
||||
it('updates provider and regenerates provider-specific core env in same settings file', () => {
|
||||
const result = updateVariant('demo', {
|
||||
provider: 'codex',
|
||||
|
||||
@@ -91,6 +91,33 @@ describe('cliproxy-stats-routes model update canonicalization', () => {
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY 4.5 model updates', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'cliproxy', 'agy.settings.json');
|
||||
writeSettings(settingsPath, {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5',
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/cliproxy/models/agy`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: 'claude-opus-4.5' }),
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
const body = (await response.json()) as { error: string };
|
||||
expect(body.error).toContain('denylist');
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||
env: Record<string, string>;
|
||||
};
|
||||
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('canonicalizes Codex effort suffix and syncs linked core model env vars', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'cliproxy', 'codex.settings.json');
|
||||
writeSettings(settingsPath, {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
createSettingsFile,
|
||||
updateSettingsFile,
|
||||
} from '../../../src/web-server/routes/route-helpers';
|
||||
|
||||
describe('route-helpers AGY denylist', () => {
|
||||
let tempHome = '';
|
||||
let originalCcsHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-route-helpers-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome === undefined) {
|
||||
delete process.env.CCS_HOME;
|
||||
} else {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
}
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY models on settings create', () => {
|
||||
expect(() =>
|
||||
createSettingsFile(
|
||||
'agy-denied',
|
||||
'http://127.0.0.1:8317/api/provider/agy',
|
||||
'test-token',
|
||||
{
|
||||
model: 'claude-sonnet-4.5',
|
||||
opusModel: 'claude-opus-4.5',
|
||||
sonnetModel: 'claude-sonnet-4.5',
|
||||
haikuModel: 'claude-haiku-4.5',
|
||||
}
|
||||
)
|
||||
).toThrow(/denylist/i);
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY models on settings update', () => {
|
||||
const settingsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(settingsDir, { recursive: true });
|
||||
const settingsPath = path.join(settingsDir, 'agy-profile.settings.json');
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'test-token',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
|
||||
expect(() => updateSettingsFile('agy-profile', { model: 'claude-opus-4.5' })).toThrow(
|
||||
/denylist/i
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -324,6 +324,88 @@ describe('settings-routes model canonicalization', () => {
|
||||
expect(persisted.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('claude-haiku-4-5');
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY models on PUT /:profile', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
writeSettings(settingsPath, { env: {} });
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4.5',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-6-thinking',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4.5',
|
||||
},
|
||||
presets: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = (await response.json()) as { error?: string };
|
||||
expect(body.error).toContain('denylist');
|
||||
});
|
||||
|
||||
it('rejects denylisted AGY models on POST /:profile/presets', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
writeSettings(settingsPath, {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
},
|
||||
presets: [],
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'legacy',
|
||||
default: 'claude-opus-4.5-thinking',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
const body = (await response.json()) as { error?: string };
|
||||
expect(body.error).toContain('denylist');
|
||||
});
|
||||
|
||||
it('drops denylisted AGY presets on read canonicalization', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
writeSettings(settingsPath, {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'denylisted',
|
||||
default: 'claude-sonnet-4.5',
|
||||
opus: 'claude-opus-4-6-thinking',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const body = (await response.json()) as { presets: Array<Record<string, string>> };
|
||||
expect(body.presets.length).toBe(0);
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||
presets: Array<Record<string, string>>;
|
||||
};
|
||||
expect(persisted.presets.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not crash on malformed non-string preset model values', async () => {
|
||||
const settingsPath = path.join(tempHome, '.ccs', 'agy.settings.json');
|
||||
writeSettings(settingsPath, {
|
||||
|
||||
Reference in new issue
Block a user