mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 20:17:45 +00:00
fix(cliproxy): cover managed prefix review follow-up
This commit is contained in:
@@ -36,7 +36,7 @@ export async function showHelp(): Promise<void> {
|
||||
[
|
||||
'Catalog Commands:',
|
||||
[
|
||||
['catalog', 'Show catalog status (cached vs static)'],
|
||||
['catalog', 'Show catalog status, routing hints, and pinned short prefixes'],
|
||||
['catalog refresh', 'Sync models from remote CLIProxy'],
|
||||
['catalog reset', 'Clear cache, revert to static catalog'],
|
||||
],
|
||||
@@ -85,7 +85,7 @@ export async function showHelp(): Promise<void> {
|
||||
[
|
||||
['--backend <type>', 'Use specific backend: original | plus (default: from config)'],
|
||||
['--target <cli>', 'Default target for created/edited variants: claude | droid'],
|
||||
['--verbose, -v', 'Show detailed quota fetch diagnostics'],
|
||||
['--verbose, -v', 'Show detailed diagnostics including routing hints and quota fetches'],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -100,6 +100,7 @@ export async function showHelp(): Promise<void> {
|
||||
}
|
||||
|
||||
console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.'));
|
||||
console.log(dim(' Routing: use gcli/<model> or agy/<model> to keep overlapping models pinned.'));
|
||||
console.log('');
|
||||
console.log(subheader('Notes:'));
|
||||
console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`);
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { afterEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { ensureManagedModelPrefixes } from '../../../src/cliproxy/managed-model-prefixes';
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
interface MockAuthFileRecord {
|
||||
account_type?: string;
|
||||
name: string;
|
||||
provider?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface DownloadResponse {
|
||||
body: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
function textResponse(body: string, status = 200): Response {
|
||||
return new Response(body, {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
function installFetchMock(options: {
|
||||
files: MockAuthFileRecord[];
|
||||
downloads?: Record<string, DownloadResponse | Error>;
|
||||
patchStatuses?: Record<string, number>;
|
||||
}) {
|
||||
const requests: Array<{ url: string; method: string; body: string | undefined }> = [];
|
||||
|
||||
global.fetch = mock((input: string | URL | Request, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const method = init?.method ?? 'GET';
|
||||
const body = typeof init?.body === 'string' ? init.body : undefined;
|
||||
|
||||
requests.push({ url, method, body });
|
||||
|
||||
if (url.endsWith('/v0/management/auth-files') && method === 'GET') {
|
||||
return Promise.resolve(jsonResponse({ files: options.files }));
|
||||
}
|
||||
|
||||
if (url.includes('/v0/management/auth-files/download') && method === 'GET') {
|
||||
const name = new URL(url).searchParams.get('name');
|
||||
if (!name) {
|
||||
return Promise.reject(new Error(`Missing auth file name for ${url}`));
|
||||
}
|
||||
|
||||
const response = options.downloads?.[name];
|
||||
if (response instanceof Error) {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
if (!response) {
|
||||
return Promise.resolve(textResponse('{}', 404));
|
||||
}
|
||||
|
||||
return Promise.resolve(textResponse(response.body, response.status ?? 200));
|
||||
}
|
||||
|
||||
if (url.endsWith('/v0/management/auth-files/fields') && method === 'PATCH') {
|
||||
const payload = JSON.parse(body ?? '{}') as { name?: string };
|
||||
const status = (payload.name && options.patchStatuses?.[payload.name]) ?? 200;
|
||||
return Promise.resolve(jsonResponse({ ok: status < 400 }, status));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unexpected fetch ${method} ${url}`));
|
||||
}) as typeof fetch;
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
describe('ensureManagedModelPrefixes', () => {
|
||||
it('patches missing managed prefixes for matching oauth providers', async () => {
|
||||
const requests = installFetchMock({
|
||||
files: [
|
||||
{ account_type: 'oauth', name: 'gemini-main', provider: 'gemini' },
|
||||
{ account_type: 'oauth', name: 'agy-main', provider: 'antigravity' },
|
||||
{ account_type: 'apikey', name: 'gemini-key', provider: 'gemini' },
|
||||
],
|
||||
downloads: {
|
||||
'gemini-main': { body: JSON.stringify({ prefix: null, provider: 'gemini' }) },
|
||||
'agy-main': { body: JSON.stringify({ prefix: null, provider: 'antigravity' }) },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await ensureManagedModelPrefixes(['gemini']);
|
||||
|
||||
expect(result).toEqual({ checked: 1, updated: 1 });
|
||||
|
||||
const patchRequest = requests.find(
|
||||
(request) =>
|
||||
request.url.endsWith('/v0/management/auth-files/fields') && request.method === 'PATCH'
|
||||
);
|
||||
expect(patchRequest?.body).toBe(JSON.stringify({ name: 'gemini-main', prefix: 'gcli' }));
|
||||
|
||||
const downloadedNames = requests
|
||||
.filter((request) => request.url.includes('/v0/management/auth-files/download'))
|
||||
.map((request) => new URL(request.url).searchParams.get('name'));
|
||||
expect(downloadedNames).toEqual(['gemini-main']);
|
||||
});
|
||||
|
||||
it('skips files that already have the managed prefix or a different custom prefix', async () => {
|
||||
const requests = installFetchMock({
|
||||
files: [
|
||||
{ account_type: 'oauth', name: 'gemini-managed', provider: 'gemini' },
|
||||
{ account_type: 'oauth', name: 'gemini-custom', provider: 'gemini' },
|
||||
],
|
||||
downloads: {
|
||||
'gemini-managed': { body: JSON.stringify({ prefix: 'gcli', provider: 'gemini' }) },
|
||||
'gemini-custom': { body: JSON.stringify({ prefix: 'team-a', provider: 'gemini' }) },
|
||||
},
|
||||
});
|
||||
|
||||
const result = await ensureManagedModelPrefixes(['gemini']);
|
||||
|
||||
expect(result).toEqual({ checked: 2, updated: 0 });
|
||||
expect(
|
||||
requests.some(
|
||||
(request) =>
|
||||
request.url.endsWith('/v0/management/auth-files/fields') && request.method === 'PATCH'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('skips patching when the downloaded auth file belongs to a different provider', async () => {
|
||||
const requests = installFetchMock({
|
||||
files: [{ account_type: 'oauth', name: 'gemini-shadowed', provider: 'gemini' }],
|
||||
downloads: {
|
||||
'gemini-shadowed': {
|
||||
body: JSON.stringify({ prefix: null, provider: 'antigravity' }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await ensureManagedModelPrefixes(['gemini']);
|
||||
|
||||
expect(result).toEqual({ checked: 1, updated: 0 });
|
||||
expect(
|
||||
requests.some(
|
||||
(request) =>
|
||||
request.url.endsWith('/v0/management/auth-files/fields') && request.method === 'PATCH'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('swallows read and patch failures so later files can still be repaired', async () => {
|
||||
const requests = installFetchMock({
|
||||
files: [
|
||||
{ account_type: 'oauth', name: 'gemini-unreadable', provider: 'gemini' },
|
||||
{ account_type: 'oauth', name: 'gemini-patch-fails', provider: 'gemini' },
|
||||
{ account_type: 'oauth', name: 'gemini-success', provider: 'gemini' },
|
||||
],
|
||||
downloads: {
|
||||
'gemini-unreadable': new Error('network down'),
|
||||
'gemini-patch-fails': { body: JSON.stringify({ prefix: null, provider: 'gemini' }) },
|
||||
'gemini-success': { body: JSON.stringify({ prefix: null, provider: 'gemini' }) },
|
||||
},
|
||||
patchStatuses: {
|
||||
'gemini-patch-fails': 500,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await ensureManagedModelPrefixes(['gemini']);
|
||||
|
||||
expect(result).toEqual({ checked: 3, updated: 1 });
|
||||
|
||||
const patchPayloads = requests
|
||||
.filter(
|
||||
(request) =>
|
||||
request.url.endsWith('/v0/management/auth-files/fields') && request.method === 'PATCH'
|
||||
)
|
||||
.map((request) => request.body);
|
||||
expect(patchPayloads).toEqual([
|
||||
JSON.stringify({ name: 'gemini-patch-fails', prefix: 'gcli' }),
|
||||
JSON.stringify({ name: 'gemini-success', prefix: 'gcli' }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user