From 8ef703024b7972d6c369cc13c6ef3d9f08f10a26 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 28 Apr 2026 14:02:58 -0400 Subject: [PATCH 1/3] fix(cliproxy): probe routing via management endpoint --- src/cliproxy/management-api-client.ts | 5 +- src/cliproxy/routing-strategy-http.ts | 3 +- .../cliproxy/management-api-client.test.ts | 2 +- .../cliproxy/routing-strategy-http.test.ts | 65 +++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 tests/unit/cliproxy/routing-strategy-http.test.ts diff --git a/src/cliproxy/management-api-client.ts b/src/cliproxy/management-api-client.ts index dc9de153..f7d5c9d7 100644 --- a/src/cliproxy/management-api-client.ts +++ b/src/cliproxy/management-api-client.ts @@ -20,6 +20,7 @@ import type { CliproxyRoutingStrategy } from './types'; /** Default timeout for management operations (longer than health check) */ const DEFAULT_TIMEOUT_MS = 5000; +const ROUTING_STRATEGY_PATH = '/v0/management/routing/strategy'; /** Default port for HTTPS protocol */ const DEFAULT_HTTPS_PORT = 443; @@ -232,7 +233,7 @@ export class ManagementApiClient { * Get the global credential routing strategy from CLIProxy. */ async getRoutingStrategy(): Promise { - const response = await this.request<{ strategy?: string }>('GET', '/routing/strategy'); + const response = await this.request<{ strategy?: string }>('GET', ROUTING_STRATEGY_PATH); return response.data?.strategy === 'fill-first' ? 'fill-first' : 'round-robin'; } @@ -240,7 +241,7 @@ export class ManagementApiClient { * Update the global credential routing strategy on CLIProxy. */ async putRoutingStrategy(strategy: CliproxyRoutingStrategy): Promise { - await this.request('PUT', '/routing/strategy', { value: strategy }); + await this.request('PUT', ROUTING_STRATEGY_PATH, { value: strategy }); return strategy; } diff --git a/src/cliproxy/routing-strategy-http.ts b/src/cliproxy/routing-strategy-http.ts index 33d7b070..b0499fb0 100644 --- a/src/cliproxy/routing-strategy-http.ts +++ b/src/cliproxy/routing-strategy-http.ts @@ -7,13 +7,14 @@ import { } from './proxy-target-resolver'; const ROUTING_TIMEOUT_MS = 5000; +const CLIPROXY_ROUTING_MANAGEMENT_PATH = '/v0/management/routing/strategy'; export async function fetchCliproxyRoutingResponse( target: ProxyTarget, method: 'GET' | 'PUT', body?: Record ): Promise { - const url = buildProxyUrl(target, '/routing/strategy'); + const url = buildProxyUrl(target, CLIPROXY_ROUTING_MANAGEMENT_PATH); const headers = buildManagementHeaders( target, body ? { 'Content-Type': 'application/json' } : {} diff --git a/tests/unit/cliproxy/management-api-client.test.ts b/tests/unit/cliproxy/management-api-client.test.ts index 12641a25..df498534 100644 --- a/tests/unit/cliproxy/management-api-client.test.ts +++ b/tests/unit/cliproxy/management-api-client.test.ts @@ -128,7 +128,7 @@ describe('management-api-client', () => { expect(strategy).toBe('round-robin'); expect(fetchMock).toHaveBeenCalledWith( - 'http://localhost:8317/routing/strategy', + 'http://localhost:8317/v0/management/routing/strategy', expect.objectContaining({ method: 'PUT', body: JSON.stringify({ value: 'round-robin' }), diff --git a/tests/unit/cliproxy/routing-strategy-http.test.ts b/tests/unit/cliproxy/routing-strategy-http.test.ts new file mode 100644 index 00000000..d3887ada --- /dev/null +++ b/tests/unit/cliproxy/routing-strategy-http.test.ts @@ -0,0 +1,65 @@ +import { afterEach, describe, expect, it, mock } from 'bun:test'; +import { fetchCliproxyRoutingResponse } from '../../../src/cliproxy/routing-strategy-http'; +import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver'; + +describe('routing-strategy-http', () => { + const target: ProxyTarget = { + host: '127.0.0.1', + port: 8317, + protocol: 'http', + isRemote: false, + }; + + afterEach(() => { + mock.restore(); + }); + + it('reads the routing strategy from the management endpoint', async () => { + const originalFetch = global.fetch; + const fetchMock = mock(() => + Promise.resolve( + new Response(JSON.stringify({ strategy: 'round-robin' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + ); + global.fetch = fetchMock as typeof global.fetch; + + await fetchCliproxyRoutingResponse(target, 'GET'); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:8317/v0/management/routing/strategy', + expect.objectContaining({ + method: 'GET', + }) + ); + + global.fetch = originalFetch; + }); + + it('writes the routing strategy to the management endpoint', async () => { + const originalFetch = global.fetch; + const fetchMock = mock(() => + Promise.resolve( + new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + ); + global.fetch = fetchMock as typeof global.fetch; + + await fetchCliproxyRoutingResponse(target, 'PUT', { value: 'fill-first' }); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://127.0.0.1:8317/v0/management/routing/strategy', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ value: 'fill-first' }), + }) + ); + + global.fetch = originalFetch; + }); +}); From aa7f15516157d55ddf40c723bf72cbe1e3e35cc6 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 28 Apr 2026 14:07:15 -0400 Subject: [PATCH 2/3] test(cliproxy): stabilize routing endpoint coverage --- src/cliproxy/routing-strategy-http.ts | 6 +- .../cliproxy/routing-strategy-http.test.ts | 79 ++++++------------- 2 files changed, 28 insertions(+), 57 deletions(-) diff --git a/src/cliproxy/routing-strategy-http.ts b/src/cliproxy/routing-strategy-http.ts index b0499fb0..81eeb558 100644 --- a/src/cliproxy/routing-strategy-http.ts +++ b/src/cliproxy/routing-strategy-http.ts @@ -9,12 +9,16 @@ import { const ROUTING_TIMEOUT_MS = 5000; const CLIPROXY_ROUTING_MANAGEMENT_PATH = '/v0/management/routing/strategy'; +export function getCliproxyRoutingManagementUrl(target: ProxyTarget): string { + return buildProxyUrl(target, CLIPROXY_ROUTING_MANAGEMENT_PATH); +} + export async function fetchCliproxyRoutingResponse( target: ProxyTarget, method: 'GET' | 'PUT', body?: Record ): Promise { - const url = buildProxyUrl(target, CLIPROXY_ROUTING_MANAGEMENT_PATH); + const url = getCliproxyRoutingManagementUrl(target); const headers = buildManagementHeaders( target, body ? { 'Content-Type': 'application/json' } : {} diff --git a/tests/unit/cliproxy/routing-strategy-http.test.ts b/tests/unit/cliproxy/routing-strategy-http.test.ts index d3887ada..6a3e9463 100644 --- a/tests/unit/cliproxy/routing-strategy-http.test.ts +++ b/tests/unit/cliproxy/routing-strategy-http.test.ts @@ -1,65 +1,32 @@ -import { afterEach, describe, expect, it, mock } from 'bun:test'; -import { fetchCliproxyRoutingResponse } from '../../../src/cliproxy/routing-strategy-http'; +import { describe, expect, it } from 'bun:test'; +import { getCliproxyRoutingManagementUrl } from '../../../src/cliproxy/routing-strategy-http'; import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver'; describe('routing-strategy-http', () => { - const target: ProxyTarget = { - host: '127.0.0.1', - port: 8317, - protocol: 'http', - isRemote: false, - }; + it('builds the local management URL for routing strategy reads', () => { + const target: ProxyTarget = { + host: '127.0.0.1', + port: 8317, + protocol: 'http', + isRemote: false, + }; - afterEach(() => { - mock.restore(); + expect(getCliproxyRoutingManagementUrl(target)).toBe( + 'http://127.0.0.1:8317/v0/management/routing/strategy' + ); }); - it('reads the routing strategy from the management endpoint', async () => { - const originalFetch = global.fetch; - const fetchMock = mock(() => - Promise.resolve( - new Response(JSON.stringify({ strategy: 'round-robin' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - ) + it('builds the remote management URL for routing strategy writes', () => { + const target: ProxyTarget = { + host: 'proxy.example.com', + port: 443, + protocol: 'https', + allowSelfSigned: true, + isRemote: true, + }; + + expect(getCliproxyRoutingManagementUrl(target)).toBe( + 'https://proxy.example.com:443/v0/management/routing/strategy' ); - global.fetch = fetchMock as typeof global.fetch; - - await fetchCliproxyRoutingResponse(target, 'GET'); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:8317/v0/management/routing/strategy', - expect.objectContaining({ - method: 'GET', - }) - ); - - global.fetch = originalFetch; - }); - - it('writes the routing strategy to the management endpoint', async () => { - const originalFetch = global.fetch; - const fetchMock = mock(() => - Promise.resolve( - new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - ) - ); - global.fetch = fetchMock as typeof global.fetch; - - await fetchCliproxyRoutingResponse(target, 'PUT', { value: 'fill-first' }); - - expect(fetchMock).toHaveBeenCalledWith( - 'http://127.0.0.1:8317/v0/management/routing/strategy', - expect.objectContaining({ - method: 'PUT', - body: JSON.stringify({ value: 'fill-first' }), - }) - ); - - global.fetch = originalFetch; }); }); From 99d814c9baf0f9e0df63d6b0c611cac13cccb382 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Apr 2026 18:17:04 +0000 Subject: [PATCH 3/3] chore(release): 7.74.0-dev.17 [skip ci] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f240e73..0c0c9774 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.74.0-dev.16", + "version": "7.74.0-dev.17", "description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more", "keywords": [ "cli",