From b6aa6fb0d4471b68fb795ef4ba2449f2a9f72788 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 9 Apr 2026 18:41:14 -0400 Subject: [PATCH] fix(cliproxy): cover managed prefix review follow-up --- src/commands/cliproxy/help-subcommand.ts | 5 +- .../cliproxy/managed-model-prefixes.test.ts | 190 ++++++++++++++++++ 2 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 tests/unit/cliproxy/managed-model-prefixes.test.ts diff --git a/src/commands/cliproxy/help-subcommand.ts b/src/commands/cliproxy/help-subcommand.ts index 9e1eae44..56a2863d 100644 --- a/src/commands/cliproxy/help-subcommand.ts +++ b/src/commands/cliproxy/help-subcommand.ts @@ -36,7 +36,7 @@ export async function showHelp(): Promise { [ '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 { [ ['--backend ', 'Use specific backend: original | plus (default: from config)'], ['--target ', '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 { } console.log(dim(' Note: CLIProxy now persists by default. Use "stop" to terminate.')); + console.log(dim(' Routing: use gcli/ or agy/ to keep overlapping models pinned.')); console.log(''); console.log(subheader('Notes:')); console.log(` Default fallback version: ${color(getFallbackVersion(), 'info')}`); diff --git a/tests/unit/cliproxy/managed-model-prefixes.test.ts b/tests/unit/cliproxy/managed-model-prefixes.test.ts new file mode 100644 index 00000000..b79f8678 --- /dev/null +++ b/tests/unit/cliproxy/managed-model-prefixes.test.ts @@ -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; + patchStatuses?: Record; +}) { + 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' }), + ]); + }); +});