mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
fix(agy): harden preset canonicalization and migration coverage
This commit is contained in:
1 parent
8ddd3596e9
commit
747c706369
9 files changed
+338
-26
No files matched your search
@@ -800,5 +800,35 @@ oauth-model-alias:
|
||||
'Should remove deprecated gemini-claude alias after regeneration'
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes dotted deprecated sonnet thinking aliases during regeneration', () => {
|
||||
const cliproxyDir = path.join(testDir, '.ccs', 'cliproxy');
|
||||
fs.mkdirSync(cliproxyDir, { recursive: true });
|
||||
|
||||
const initialConfig = `# CLIProxyAPI config generated by CCS v11
|
||||
port: 8317
|
||||
api-keys:
|
||||
- "ccs-internal-managed"
|
||||
auth-dir: "${cliproxyDir.replace(/\\/g, '/')}/auth"
|
||||
oauth-model-alias:
|
||||
antigravity:
|
||||
- name: claude-sonnet-4-6-thinking
|
||||
alias: claude-sonnet-4.6-thinking
|
||||
fork: true
|
||||
`;
|
||||
fs.writeFileSync(path.join(cliproxyDir, 'config.yaml'), initialConfig);
|
||||
|
||||
regenerateConfig();
|
||||
|
||||
const newConfig = fs.readFileSync(path.join(cliproxyDir, 'config.yaml'), 'utf-8');
|
||||
assert(
|
||||
newConfig.includes('alias: claude-sonnet-4-6'),
|
||||
'Should migrate dotted deprecated sonnet thinking alias to canonical model ID'
|
||||
);
|
||||
assert(
|
||||
!newConfig.includes('alias: claude-sonnet-4.6-thinking'),
|
||||
'Should remove dotted deprecated sonnet thinking alias after regeneration'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -245,6 +245,15 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4.6-thinking',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-sonnet-4-5',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'legacy',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
haiku: 'claude-sonnet-4-5',
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
@@ -255,11 +264,63 @@ describe('getEffectiveEnvVars local provider URL normalization', () => {
|
||||
|
||||
const repaired = JSON.parse(fs.readFileSync(agySettingsPath, 'utf-8')) as {
|
||||
env?: Record<string, string>;
|
||||
presets?: Array<Record<string, string>>;
|
||||
};
|
||||
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.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');
|
||||
});
|
||||
|
||||
it('migrates codex effort-suffixed preset IDs during ensureProviderSettings', () => {
|
||||
process.env.CCS_HOME = tempHome;
|
||||
const codexSettingsPath = path.join(tempHome, '.ccs', 'codex.settings.json');
|
||||
fs.mkdirSync(path.dirname(codexSettingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
codexSettingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/codex',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: ' gpt-5.3-codex-xhigh ',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gpt-5.3-codex-xhigh',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gpt-5.3-codex-high',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gpt-5-mini-medium',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'legacy',
|
||||
default: 'gpt-5.3-codex-xhigh',
|
||||
opus: 'gpt-5.3-codex-xhigh',
|
||||
sonnet: 'gpt-5.3-codex-high',
|
||||
haiku: 'gpt-5-mini-medium',
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
ensureProviderSettings('codex');
|
||||
|
||||
const repaired = JSON.parse(fs.readFileSync(codexSettingsPath, 'utf-8')) as {
|
||||
env?: Record<string, string>;
|
||||
presets?: Array<Record<string, string>>;
|
||||
};
|
||||
expect(repaired.env?.ANTHROPIC_MODEL).toBe('gpt-5.3-codex');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.3-codex');
|
||||
expect(repaired.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5-mini');
|
||||
expect(repaired.presets?.[0]?.default).toBe('gpt-5.3-codex');
|
||||
expect(repaired.presets?.[0]?.opus).toBe('gpt-5.3-codex');
|
||||
expect(repaired.presets?.[0]?.sonnet).toBe('gpt-5.3-codex');
|
||||
expect(repaired.presets?.[0]?.haiku).toBe('gpt-5-mini');
|
||||
});
|
||||
|
||||
it('recovers malformed provider settings files by writing defaults and backup copy', () => {
|
||||
|
||||
@@ -14,4 +14,12 @@ describe('model-catalog compatibility lookups', () => {
|
||||
expect(supportsThinking('agy', 'claude-opus-4.6-thinking')).toBe(true);
|
||||
expect(supportsThinking('agy', 'claude-sonnet-4.5')).toBe(false);
|
||||
});
|
||||
|
||||
it('maps legacy sonnet 4.6 thinking aliases to canonical agy model', () => {
|
||||
const dottedLegacy = findModel('agy', 'claude-sonnet-4.6-thinking');
|
||||
const hyphenLegacy = findModel('agy', 'claude-sonnet-4-6-thinking');
|
||||
|
||||
expect(dottedLegacy?.id).toBe('claude-sonnet-4-6');
|
||||
expect(hyphenLegacy?.id).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
});
|
||||
@@ -78,6 +78,21 @@ describe('model-id-normalizer', () => {
|
||||
'claude-sonnet-4-6-thinking'
|
||||
);
|
||||
});
|
||||
|
||||
it('trims and canonicalizes provider model IDs with surrounding whitespace', () => {
|
||||
expect(canonicalizeModelIdForProvider(' gpt-5.3-codex-high ', 'codex')).toBe(
|
||||
'gpt-5.3-codex'
|
||||
);
|
||||
expect(canonicalizeModelIdForProvider(' claude-sonnet-4.6-thinking ', 'agy')).toBe(
|
||||
'claude-sonnet-4-6'
|
||||
);
|
||||
expect(canonicalizeModelIdForProvider(' claude-sonnet-4-6-thinking ', 'claude')).toBe(
|
||||
'claude-sonnet-4-6-thinking'
|
||||
);
|
||||
expect(normalizeModelIdForRouting(' claude-sonnet-4.6-thinking ', null)).toBe(
|
||||
'claude-sonnet-4-6'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('env normalization', () => {
|
||||
|
||||
@@ -115,7 +115,15 @@ describe('settings-routes model canonicalization', () => {
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/agy',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
},
|
||||
presets: [],
|
||||
presets: [
|
||||
{
|
||||
name: 'legacy-existing',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets`, {
|
||||
@@ -144,6 +152,144 @@ describe('settings-routes model canonicalization', () => {
|
||||
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-haiku-4-5');
|
||||
expect(persisted.presets[1]?.default).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets[1]?.opus).toBe('claude-opus-4-6-thinking');
|
||||
expect(persisted.presets[1]?.sonnet).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets[1]?.haiku).toBe('claude-haiku-4-5');
|
||||
});
|
||||
|
||||
it('canonicalizes and persists AGY preset aliases on GET /: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',
|
||||
ANTHROPIC_MODEL: 'claude-sonnet-4.6-thinking',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'legacy',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
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[0]?.default).toBe('claude-sonnet-4-6');
|
||||
expect(body.presets[0]?.opus).toBe('claude-opus-4-6-thinking');
|
||||
expect(body.presets[0]?.sonnet).toBe('claude-sonnet-4-6');
|
||||
expect(body.presets[0]?.haiku).toBe('claude-haiku-4-5');
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||
env: Record<string, string>;
|
||||
presets: Array<Record<string, string>>;
|
||||
};
|
||||
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets[0]?.default).toBe('claude-sonnet-4-6');
|
||||
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-haiku-4-5');
|
||||
});
|
||||
|
||||
it('canonicalizes existing settings on DELETE /:profile/presets/:name writes', 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-thinking',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'remove-me',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets/remove-me`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const persisted = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
|
||||
env: Record<string, string>;
|
||||
presets: Array<Record<string, string>>;
|
||||
};
|
||||
expect(persisted.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6');
|
||||
expect(persisted.presets.length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not mutate settings on duplicate preset POST failure', 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-thinking',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'existing',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
],
|
||||
});
|
||||
const before = fs.readFileSync(settingsPath, 'utf8');
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: 'existing',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
}),
|
||||
});
|
||||
expect(response.status).toBe(409);
|
||||
|
||||
const after = fs.readFileSync(settingsPath, 'utf8');
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
|
||||
it('does not mutate settings on missing preset DELETE failure', 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-thinking',
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
name: 'keep-me',
|
||||
default: 'claude-sonnet-4-6-thinking',
|
||||
opus: 'claude-opus-4.6-thinking',
|
||||
sonnet: 'claude-sonnet-4.6-thinking',
|
||||
haiku: 'claude-haiku-4.5',
|
||||
},
|
||||
],
|
||||
});
|
||||
const before = fs.readFileSync(settingsPath, 'utf8');
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/settings/agy/presets/does-not-exist`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
|
||||
const after = fs.readFileSync(settingsPath, 'utf8');
|
||||
expect(after).toBe(before);
|
||||
});
|
||||
|
||||
it('canonicalizes AGY aliases via provider alias profile names', async () => {
|
||||
|
||||
Reference in new issue
Block a user