From 3658c0113729514dc660af99ade379644c57186d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:09:34 -0400 Subject: [PATCH 1/7] feat(cliproxy): add meetsBackendMinimumVersion comparator helper --- .../__tests__/version-checker-suffix.test.ts | 42 ++++++++++++++++++- src/cliproxy/binary/version-checker.ts | 10 +++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts b/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts index 186942b0..c20c75ae 100644 --- a/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts +++ b/src/cliproxy/binary/__tests__/version-checker-suffix.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'bun:test'; -import { compareCliproxyVersions, isNewerVersion } from '../version-checker'; +import { + type CLIProxyBackendMinVersions, + compareCliproxyVersions, + isNewerVersion, + meetsBackendMinimumVersion, +} from '../version-checker'; describe('cliproxy version comparison', () => { it('treats missing fork release suffix as zero', () => { @@ -19,3 +24,38 @@ describe('cliproxy version comparison', () => { expect(isNewerVersion('7.1.31-99', '7.1.32-0')).toBe(false); }); }); + +describe('meetsBackendMinimumVersion', () => { + const testMinimums: CLIProxyBackendMinVersions = { + original: '7.2.104', + plus: '7.2.105-0', + }; + + it('evaluates original backend boundary correctly', () => { + expect(meetsBackendMinimumVersion('7.2.103', 'original', testMinimums)).toBe(false); + expect(meetsBackendMinimumVersion('7.2.104', 'original', testMinimums)).toBe(true); + expect(meetsBackendMinimumVersion('7.2.105', 'original', testMinimums)).toBe(true); + }); + + it('evaluates plus backend boundary with fork suffixes correctly', () => { + expect(meetsBackendMinimumVersion('7.2.104-9', 'plus', testMinimums)).toBe(false); + expect(meetsBackendMinimumVersion('7.2.105-0', 'plus', testMinimums)).toBe(true); + expect(meetsBackendMinimumVersion('7.2.105-1', 'plus', testMinimums)).toBe(true); + }); + + it('treats missing fork suffix as equal to -0', () => { + const minWithSuffix: CLIProxyBackendMinVersions = { + original: '6.8.34-0', + plus: '6.8.34-0', + }; + expect(meetsBackendMinimumVersion('6.8.34', 'original', minWithSuffix)).toBe(true); + expect(meetsBackendMinimumVersion('6.8.34', 'plus', minWithSuffix)).toBe(true); + }); + + it('rejects empty and invalid version strings against real minimums', () => { + expect(meetsBackendMinimumVersion('', 'original', testMinimums)).toBe(false); + expect(meetsBackendMinimumVersion('not-a-version', 'original', testMinimums)).toBe(false); + expect(meetsBackendMinimumVersion('', 'plus', testMinimums)).toBe(false); + expect(meetsBackendMinimumVersion('not-a-version', 'plus', testMinimums)).toBe(false); + }); +}); diff --git a/src/cliproxy/binary/version-checker.ts b/src/cliproxy/binary/version-checker.ts index f4e7c547..946dfc78 100644 --- a/src/cliproxy/binary/version-checker.ts +++ b/src/cliproxy/binary/version-checker.ts @@ -21,6 +21,8 @@ import { } from '../binary/platform-detector'; import type { CLIProxyBackend } from '../types'; +export type CLIProxyBackendMinVersions = Readonly>; + interface FetchLatestVersionDeps { fetchJsonFn?: typeof fetchJson; } @@ -78,6 +80,14 @@ export function isNewerVersion(latest: string, current: string): boolean { return compareCliproxyVersions(latest, current) > 0; } +export function meetsBackendMinimumVersion( + installedVersion: string, + backend: CLIProxyBackend, + minimumByBackend: CLIProxyBackendMinVersions +): boolean { + return compareCliproxyVersions(installedVersion, minimumByBackend[backend]) >= 0; +} + /** * Check if version is within the faulty range (v81-85) * @returns true if version has known critical bugs From 87f1d9b4e874f63a61d337357796db8e27a4f18f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:10:29 -0400 Subject: [PATCH 2/7] fix(cliproxy): correct ARM release asset architecture boundaries (#1725) --- src/cliproxy/binary/platform-detector.ts | 8 +- .../__tests__/backend-selection.test.js | 73 ++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index fdb78d83..9721afb0 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -84,7 +84,10 @@ const RELEASE_ARCH_MAP: Record = { }; const PLUS_NO_PLUGIN_ASSET_MIN_VERSION = '7.1.68-0'; -const PLUS_AARCH64_ASSET_MIN_VERSION = '7.1.45-1'; +const AARCH64_ASSET_MIN_VERSION: Readonly> = { + original: '6.9.48', + plus: '6.10.0-0', +}; export function mapNodeArchToReleaseArch(nodeArch: string): SupportedArch | undefined { const arch = ARCH_MAP[nodeArch]; @@ -122,9 +125,8 @@ function getReleaseArchForBackend( releaseArch: SupportedArch ): SupportedArch { if ( - backend === 'plus' && publicArch === 'arm64' && - !isAtLeastVersion(version, PLUS_AARCH64_ASSET_MIN_VERSION) + !isAtLeastVersion(version, AARCH64_ASSET_MIN_VERSION[backend]) ) { return 'arm64'; } diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index e48d1091..54bec9a0 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -86,13 +86,84 @@ describe('Backend Selection', () => { ); }); }); - it('generates correct binary name for original backend', () => { const info = platformDetector.detectPlatform('6.6.51', 'original'); assert(info.binaryName.startsWith('CLIProxyAPI_6.6.51_')); assert(!info.binaryName.includes('CLIProxyAPIPlus')); }); + it('selects arm64 vs aarch64 release assets across original and plus boundaries', () => { + // Original pre-rename boundaries (<= 6.9.47) use arm64 + for (const version of [undefined, '6.9.45', '6.9.47']) { + const effectiveVersion = version || platformDetector.BACKEND_CONFIG.original.fallbackVersion; + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform(version, 'original'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, `CLIProxyAPI_${effectiveVersion}_darwin_arm64.tar.gz`); + assert.strictEqual( + platformDetector.getDownloadUrl(version, 'original'), + `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${effectiveVersion}/CLIProxyAPI_${effectiveVersion}_darwin_arm64.tar.gz` + ); + }); + + withMockedProcessPlatform('linux', 'arm64', () => { + const info = platformDetector.detectPlatform(version, 'original'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, `CLIProxyAPI_${effectiveVersion}_linux_arm64.tar.gz`); + assert.strictEqual( + platformDetector.getDownloadUrl(version, 'original'), + `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${effectiveVersion}/CLIProxyAPI_${effectiveVersion}_linux_arm64.tar.gz` + ); + }); + + withMockedProcessPlatform('win32', 'arm64', () => { + const info = platformDetector.detectPlatform(version, 'original'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, `CLIProxyAPI_${effectiveVersion}_windows_arm64.zip`); + assert.strictEqual( + platformDetector.getDownloadUrl(version, 'original'), + `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${effectiveVersion}/CLIProxyAPI_${effectiveVersion}_windows_arm64.zip` + ); + }); + } + + // Original post-rename boundary (>= 6.9.48) uses aarch64 while public arch remains arm64 + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform('6.9.48', 'original'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, 'CLIProxyAPI_6.9.48_darwin_aarch64.tar.gz'); + assert.strictEqual( + platformDetector.getDownloadUrl('6.9.48', 'original'), + 'https://github.com/router-for-me/CLIProxyAPI/releases/download/v6.9.48/CLIProxyAPI_6.9.48_darwin_aarch64.tar.gz' + ); + }); + + // Plus fallback / pre-rename (< 6.10.0-0) uses arm64 + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform('6.9.45-0', 'plus'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, 'CLIProxyAPIPlus_6.9.45-0_darwin_arm64.tar.gz'); + }); + + // Plus post-rename (>= 6.10.0-0) uses aarch64 + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform('6.10.0-0', 'plus'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual(info.binaryName, 'CLIProxyAPIPlus_6.10.0-0_darwin_aarch64.tar.gz'); + }); + + // x64 remains amd64 for both backends + withMockedProcessPlatform('linux', 'x64', () => { + assert.strictEqual( + platformDetector.detectPlatform('6.9.45', 'original').binaryName, + 'CLIProxyAPI_6.9.45_linux_amd64.tar.gz' + ); + assert.strictEqual( + platformDetector.detectPlatform('6.9.45-0', 'plus').binaryName, + 'CLIProxyAPIPlus_6.9.45-0_linux_amd64.tar.gz' + ); + }); + }); it('uses static no-plugin assets when the integrated Linux image opts in', () => { withEnvironmentVariable('CCS_CLIPROXY_NO_PLUGIN_ASSET', '1', () => { withMockedProcessPlatform('linux', 'x64', () => { From b9601cb913dce355e2a5f3347500efe024e79974 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:11:47 -0400 Subject: [PATCH 3/7] fix(cliproxy): correct pool routing version capability map (#1726) --- .../__tests__/routing-strategy.test.ts | 91 ++++++++++++++++++- src/cliproxy/routing/routing-strategy.ts | 38 +++++--- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/cliproxy/routing/__tests__/routing-strategy.test.ts b/src/cliproxy/routing/__tests__/routing-strategy.test.ts index 3005620f..271496e0 100644 --- a/src/cliproxy/routing/__tests__/routing-strategy.test.ts +++ b/src/cliproxy/routing/__tests__/routing-strategy.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; - +import type { CLIProxyBackend } from '../../types'; describe('cliproxy routing strategy service', () => { let tempHome = ''; let scopedConfigDir = ''; @@ -15,6 +15,10 @@ describe('cliproxy routing strategy service', () => { protocol: 'http' as const, isRemote: false, }; + let binaryBackend: CLIProxyBackend = 'original'; + let binaryVersion = '7.2.127-7'; + let installedVersionQueriedBackend: CLIProxyBackend | undefined = undefined; + let loggedWarnings: Array<{ event: string; message: string; context?: Record }> = []; let responseFactory: (() => Promise) | null = null; beforeEach(async () => { @@ -28,6 +32,10 @@ describe('cliproxy routing strategy service', () => { }; responseFactory = null; originalCcsDir = process.env.CCS_DIR; + binaryBackend = 'original'; + binaryVersion = '7.2.127-7'; + installedVersionQueriedBackend = undefined; + loggedWarnings = []; originalCcsHome = process.env.CCS_HOME; process.env.CCS_DIR = scopedConfigDir; process.env.CCS_HOME = tempHome; @@ -74,6 +82,26 @@ describe('cliproxy routing strategy service', () => { }, })); + mock.module('../../binary-manager', () => ({ + getConfiguredBackend: () => binaryBackend, + getInstalledCliproxyVersion: (backend?: CLIProxyBackend) => { + installedVersionQueriedBackend = backend; + return binaryVersion; + }, + })); + + mock.module('../../../services/logging', () => ({ + createLogger: () => ({ + debug: () => {}, + info: () => {}, + warn: (event: string, message: string, context?: Record) => { + loggedWarnings.push({ event, message, context }); + }, + error: () => {}, + child: () => ({ warn: () => {} }), + }), + })); + return import(`../routing-strategy?test=${Date.now()}-${Math.random()}`); } @@ -365,4 +393,65 @@ describe('cliproxy routing strategy service', () => { expect(result.message).not.toContain('Pool routing is active'); }); }); + + describe('pool routing version compatibility (#1726)', () => { + it('defines backend-specific minimum versions for pool routing', async () => { + const mod = await loadRoutingModule(); + expect(mod.POOL_ROUTING_MIN_VERSION).toEqual({ + original: '6.8.34', + plus: '6.8.34-0', + }); + }); + + it('evaluates isPoolRoutingSupported correctly across original and plus thresholds', async () => { + const mod = await loadRoutingModule(); + expect(mod.isPoolRoutingSupported('original', '6.8.33')).toBe(false); + expect(mod.isPoolRoutingSupported('original', '6.8.34')).toBe(true); + expect(mod.isPoolRoutingSupported('original', '6.9.0')).toBe(true); + + expect(mod.isPoolRoutingSupported('plus', '6.8.33-9')).toBe(false); + expect(mod.isPoolRoutingSupported('plus', '6.8.34-0')).toBe(true); + expect(mod.isPoolRoutingSupported('plus', '7.2.127-7')).toBe(true); + + expect(mod.isPoolRoutingSupported('original', '')).toBe(false); + expect(mod.isPoolRoutingSupported('plus', 'not-a-version')).toBe(false); + }); + + it('warns with backend-specific metadata when plus binary is below minimum', async () => { + await withScopedConfig(async () => { + binaryBackend = 'plus'; + binaryVersion = '6.8.33-9'; + loggedWarnings = []; + + const mod = await loadRoutingModule(); + const result = mod.enablePoolRouting(8317); + + expect(result.changed).toBe(true); + expect(installedVersionQueriedBackend).toBe('plus'); + expect(loggedWarnings).toHaveLength(1); + expect(loggedWarnings[0].event).toBe('pool_routing.binary_below_minimum'); + expect(loggedWarnings[0].message).toContain('CLIProxy Plus'); + expect(loggedWarnings[0].context).toEqual({ + backend: 'plus', + installedVersion: '6.8.33-9', + minimumVersion: '6.8.34-0', + }); + }); + }); + + it('does not warn when plus binary meets the minimum version', async () => { + await withScopedConfig(async () => { + binaryBackend = 'plus'; + binaryVersion = '6.8.34-0'; + loggedWarnings = []; + + const mod = await loadRoutingModule(); + const result = mod.enablePoolRouting(8317); + + expect(result.changed).toBe(true); + expect(installedVersionQueriedBackend).toBe('plus'); + expect(loggedWarnings).toHaveLength(0); + }); + }); + }); }); diff --git a/src/cliproxy/routing/routing-strategy.ts b/src/cliproxy/routing/routing-strategy.ts index d025e858..3533cef0 100644 --- a/src/cliproxy/routing/routing-strategy.ts +++ b/src/cliproxy/routing/routing-strategy.ts @@ -7,10 +7,13 @@ import { getCliproxyRoutingTarget, getRoutingErrorMessage, } from './routing-strategy-http'; -import type { CliproxyRoutingStrategy } from '../types'; +import type { CLIProxyBackend, CliproxyRoutingStrategy } from '../types'; import { loadOrCreateUnifiedConfig, mutateConfig } from '../../config/config-loader-facade'; -import { getInstalledCliproxyVersion } from '../binary-manager'; -import { compareVersions } from '../../utils/update-checker'; +import { getConfiguredBackend, getInstalledCliproxyVersion } from '../binary-manager'; +import { + type CLIProxyBackendMinVersions, + meetsBackendMinimumVersion, +} from '../binary/version-checker'; import { getConfigYamlPath } from '../../config/loader/io-locks'; import { createLogger } from '../../services/logging'; @@ -46,12 +49,18 @@ export const POOL_ROUTING_VERIFIED_PROVIDERS = new Set(['claude', 'agy']); * max-retry-credentials and the cooling flip. * Older binaries silently ignore unknown keys — pool rails would appear active * but have no effect. Warn the user at enable time if below this version. - * - * NOTE: Update this constant when upstream first ships these keys. - * Current best estimate based on spec; adjust after spike Test D confirms. */ -export const POOL_ROUTING_MIN_VERSION = '6.9.45'; +export const POOL_ROUTING_MIN_VERSION: CLIProxyBackendMinVersions = { + original: '6.8.34', + plus: '6.8.34-0', +}; +export function isPoolRoutingSupported( + backend: CLIProxyBackend, + installedVersion: string +): boolean { + return meetsBackendMinimumVersion(installedVersion, backend, POOL_ROUTING_MIN_VERSION); +} /** * Pool-active override warning text. When pool routing is enabled the generator * forces fill-first/affinity/cooling and ignores the stored strategy/affinity, so @@ -205,20 +214,21 @@ export function enablePoolRouting( const preservedExplicitSetting = hasExplicitRoutingStrategy() || hasExplicitSessionAffinity(); - // Spec step 3 / architecture: assert minimum CLIProxy version at enable time. - // Stale binaries silently ignore max-retry-credentials and the cooling flip, - // so pool rails would appear active but have no effect. Warn and proceed. try { - const installedVersion = getInstalledCliproxyVersion(); - if (compareVersions(installedVersion, POOL_ROUTING_MIN_VERSION) < 0) { + const backend = getConfiguredBackend(); + const installedVersion = getInstalledCliproxyVersion(backend); + if (!isPoolRoutingSupported(backend, installedVersion)) { + const minimumVersion = POOL_ROUTING_MIN_VERSION[backend]; + const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; logger.warn( 'pool_routing.binary_below_minimum', - `CLIProxy v${installedVersion} is older than the pool routing minimum (v${POOL_ROUTING_MIN_VERSION}). ` + + `${backendLabel} v${installedVersion} is older than the pool routing minimum (v${minimumVersion}). ` + `The max-retry-credentials and cooling keys may be silently ignored by the running binary. ` + `Run 'ccs cliproxy --latest' to update CLIProxy, then restart with 'ccs cliproxy restart'.`, { + backend, installedVersion, - minimumVersion: POOL_ROUTING_MIN_VERSION, + minimumVersion, } ); } From ed1badcfcb90f6e096ec06f28c27fecd270c0239 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:14:48 -0400 Subject: [PATCH 4/7] fix(cliproxy): fail-closed on unsupported drain-order priority selection (#1724) --- .../accounts/__tests__/drain-order.test.ts | 33 ++++ src/cliproxy/accounts/drain-order.ts | 23 ++- .../__tests__/order-subcommand.test.ts | 158 +++++++++++++++++- src/commands/cliproxy/order-subcommand.ts | 31 ++++ 4 files changed, 243 insertions(+), 2 deletions(-) diff --git a/src/cliproxy/accounts/__tests__/drain-order.test.ts b/src/cliproxy/accounts/__tests__/drain-order.test.ts index a8942d28..cc397a65 100644 --- a/src/cliproxy/accounts/__tests__/drain-order.test.ts +++ b/src/cliproxy/accounts/__tests__/drain-order.test.ts @@ -933,3 +933,36 @@ describe('fetchProxyAuthCooldowns', () => { }); }); }); + +describe('DRAIN_ORDER_MIN_VERSION and isDrainOrderSupported (#1724)', () => { + it('defines backend-specific minimum versions for drain-order priorities', async () => { + const { DRAIN_ORDER_MIN_VERSION } = await loadDrainOrder(); + expect(DRAIN_ORDER_MIN_VERSION).toEqual({ + original: '6.6.106', + plus: '6.6.107-0', + }); + }); + + it('evaluates original backend boundary correctly', async () => { + const { isDrainOrderSupported } = await loadDrainOrder(); + expect(isDrainOrderSupported('original', '6.6.105')).toBe(false); + expect(isDrainOrderSupported('original', '6.6.106')).toBe(true); + expect(isDrainOrderSupported('original', '6.7.0')).toBe(true); + }); + + it('evaluates plus backend boundary with fork suffixes correctly', async () => { + const { isDrainOrderSupported } = await loadDrainOrder(); + expect(isDrainOrderSupported('plus', '6.6.105-9')).toBe(false); + expect(isDrainOrderSupported('plus', '6.6.106-0')).toBe(false); + expect(isDrainOrderSupported('plus', '6.6.107-0')).toBe(true); + expect(isDrainOrderSupported('plus', '7.2.127-7')).toBe(true); + }); + + it('rejects invalid and empty version strings', async () => { + const { isDrainOrderSupported } = await loadDrainOrder(); + expect(isDrainOrderSupported('original', '')).toBe(false); + expect(isDrainOrderSupported('original', 'invalid')).toBe(false); + expect(isDrainOrderSupported('plus', '')).toBe(false); + expect(isDrainOrderSupported('plus', 'invalid')).toBe(false); + }); +}); diff --git a/src/cliproxy/accounts/drain-order.ts b/src/cliproxy/accounts/drain-order.ts index 6f1e82ce..b8014818 100644 --- a/src/cliproxy/accounts/drain-order.ts +++ b/src/cliproxy/accounts/drain-order.ts @@ -21,11 +21,32 @@ import { } from '../proxy/proxy-target-resolver'; import { loadDrainOrderConfig } from './registry'; import type { AccountTier } from './types'; -import type { CLIProxyProvider } from '../types'; +import type { CLIProxyBackend, CLIProxyProvider } from '../types'; +import { + type CLIProxyBackendMinVersions, + meetsBackendMinimumVersion, +} from '../binary/version-checker'; /** Minimum valid priority value. Management layer treats 0 as delete. */ export const MIN_PRIORITY = 1; +/** + * Minimum CLIProxy version that supports drain-order priority selection. + * Original: v6.6.106 + * Plus: v6.6.107-0 + */ +export const DRAIN_ORDER_MIN_VERSION: CLIProxyBackendMinVersions = { + original: '6.6.106', + plus: '6.6.107-0', +}; + +export function isDrainOrderSupported( + backend: CLIProxyBackend, + installedVersion: string +): boolean { + return meetsBackendMinimumVersion(installedVersion, backend, DRAIN_ORDER_MIN_VERSION); +} + /** Management API path for patching auth file fields */ const AUTH_FILES_FIELDS_PATH = '/v0/management/auth-files/fields'; diff --git a/src/commands/cliproxy/__tests__/order-subcommand.test.ts b/src/commands/cliproxy/__tests__/order-subcommand.test.ts index 8cc917d6..37fc0893 100644 --- a/src/commands/cliproxy/__tests__/order-subcommand.test.ts +++ b/src/commands/cliproxy/__tests__/order-subcommand.test.ts @@ -24,6 +24,7 @@ mock.module('../../../cliproxy/proxy/proxy-detector', () => ({ describe('handleOrderSubcommand', () => { let tempHome: string; let originalCcsHome: string | undefined; + let originalNoColor: string | undefined; let logSpy: ReturnType; let lines: string[]; @@ -35,7 +36,16 @@ describe('handleOrderSubcommand', () => { fs.mkdirSync(authDir(), { recursive: true, mode: 0o700 }); fs.writeFileSync( path.join(authDir(), fileName), - JSON.stringify({ type: 'claude', ...fields }, null, 2), + JSON.stringify({ type: 'claude', email: fileName, ...fields }, null, 2), + { mode: 0o600 } + ); + } + + function writeAgyAuthFile(fileName: string, fields: Record = {}): void { + fs.mkdirSync(authDir(), { recursive: true, mode: 0o700 }); + fs.writeFileSync( + path.join(authDir(), fileName), + JSON.stringify({ type: 'antigravity', email: fileName, ...fields }, null, 2), { mode: 0o600 } ); } @@ -47,6 +57,24 @@ describe('handleOrderSubcommand', () => { >; } + async function loadRegistry() { + return import(`../../../cliproxy/accounts/registry?order-subcommand-reg=${Date.now()}`); + } + + async function configureBackend(backend: 'original' | 'plus', version: string): Promise { + const { mutateConfig, invalidateConfigCache } = await import( + '../../../config/config-loader-facade' + ); + mutateConfig((cfg) => { + if (!cfg.cliproxy) cfg.cliproxy = {}; + cfg.cliproxy.backend = backend; + }); + invalidateConfigCache(); + const verPath = path.join(tempHome, '.ccs', 'cliproxy', 'bin', backend, '.version'); + fs.mkdirSync(path.dirname(verPath), { recursive: true }); + fs.writeFileSync(verPath, version.trim() + '\n', 'utf-8'); + } + async function registerClaude(): Promise<{ registerAccount: (provider: string, tokenFile: string, email: string) => unknown; saveDrainOrderConfig: (provider: string, config: unknown) => boolean; @@ -64,7 +92,9 @@ describe('handleOrderSubcommand', () => { beforeEach(() => { tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-order-subcommand-')); originalCcsHome = process.env.CCS_HOME; + originalNoColor = process.env.NO_COLOR; process.env.CCS_HOME = tempHome; + process.env.NO_COLOR = '1'; process.exitCode = 0; lines = []; logSpy = spyOn(console, 'log').mockImplementation((msg?: unknown) => { @@ -80,6 +110,11 @@ describe('handleOrderSubcommand', () => { } else { delete process.env.CCS_HOME; } + if (originalNoColor !== undefined) { + process.env.NO_COLOR = originalNoColor; + } else { + delete process.env.NO_COLOR; + } fs.rmSync(tempHome, { recursive: true, force: true }); }); @@ -164,5 +199,126 @@ describe('handleOrderSubcommand', () => { expect(output).toContain('reset to file order'); expect(output).toContain('no priority set'); }); +}); + + describe('binary version capability gate (#1724)', () => { + const cases: Array<{ + backend: 'original' | 'plus'; + belowMinVersion: string; + atMinVersion: string; + backendLabel: string; + requiredMinVersion: string; + }> = [ + { + backend: 'original', + belowMinVersion: '6.6.105', + atMinVersion: '6.6.106', + backendLabel: 'CLIProxy', + requiredMinVersion: '6.6.106', + }, + { + backend: 'plus', + belowMinVersion: '6.6.105-0', + atMinVersion: '6.6.107-0', + backendLabel: 'CLIProxy Plus', + requiredMinVersion: '6.6.107-0', + }, + ]; + + for (const { backend, belowMinVersion, atMinVersion, backendLabel, requiredMinVersion } of cases) { + describe(`${backend} backend`, () => { + it(`refuses --set below minimum version (${belowMinVersion}) without mutating files`, async () => { + await configureBackend(backend, belowMinVersion); + writeAuthFile('claude-a.json', { email: 'a@x.com' }); + writeAuthFile('claude-b.json', { email: 'b@x.com' }); + + const { registerAccount, loadDrainOrderConfig } = await loadRegistry(); + registerAccount('claude', 'claude-a.json', 'a@x.com'); + registerAccount('claude', 'claude-b.json', 'b@x.com'); + + await runOrderSubcommand(['claude', '--set', 'a@x.com,b@x.com']); + + expect(process.exitCode).toBe(1); + const output = lines.join('\n'); + expect(output).toContain('[X]'); + expect(output).toContain( + `${backendLabel} v${belowMinVersion} does not support drain-order priorities (requires v${requiredMinVersion} or newer).` + ); + expect(output).toContain( + `Run 'ccs cliproxy --latest' to update, then restart with 'ccs cliproxy restart'.` + ); + expect(output).not.toContain('Set priorities'); + expect('priority' in readAuthFile('claude-a.json')).toBe(false); + expect('priority' in readAuthFile('claude-b.json')).toBe(false); + expect(loadDrainOrderConfig('claude')).toBeUndefined(); + }); + + it(`refuses --by-tier below minimum version (${belowMinVersion}) without mutating files`, async () => { + await configureBackend(backend, belowMinVersion); + writeAgyAuthFile('antigravity-a.json', { email: 'a@x.com' }); + writeAgyAuthFile('antigravity-b.json', { email: 'b@x.com' }); + + const { registerAccount, setAccountTier, loadDrainOrderConfig } = await loadRegistry(); + registerAccount('agy', 'antigravity-a.json', 'a@x.com'); + registerAccount('agy', 'antigravity-b.json', 'b@x.com'); + setAccountTier('agy', 'a@x.com', 'pro'); + setAccountTier('agy', 'b@x.com', 'free'); + + await runOrderSubcommand(['agy', '--by-tier']); + + expect(process.exitCode).toBe(1); + const output = lines.join('\n'); + expect(output).toContain('[X]'); + expect(output).toContain( + `${backendLabel} v${belowMinVersion} does not support drain-order priorities (requires v${requiredMinVersion} or newer).` + ); + expect(output).toContain( + `Run 'ccs cliproxy --latest' to update, then restart with 'ccs cliproxy restart'.` + ); + expect(output).not.toContain('Set priorities'); + expect('priority' in readAuthFile('antigravity-a.json')).toBe(false); + expect('priority' in readAuthFile('antigravity-b.json')).toBe(false); + expect(loadDrainOrderConfig('agy')).toBeUndefined(); + }); + + it(`applies and persists --set at minimum version (${atMinVersion})`, async () => { + await configureBackend(backend, atMinVersion); + writeAuthFile('claude-a.json', { email: 'a@x.com' }); + writeAuthFile('claude-b.json', { email: 'b@x.com' }); + + const { registerAccount, loadDrainOrderConfig } = await loadRegistry(); + registerAccount('claude', 'claude-a.json', 'a@x.com'); + registerAccount('claude', 'claude-b.json', 'b@x.com'); + + await runOrderSubcommand(['claude', '--set', 'a@x.com,b@x.com']); + + expect(process.exitCode).toBe(0); + const output = lines.join('\n'); + expect(output).toContain('Set priorities for 2 account(s).'); + expect('priority' in readAuthFile('claude-a.json')).toBe(true); + expect(loadDrainOrderConfig('claude')?.mode).toBe('manual'); + }); + + it(`applies and persists --by-tier at minimum version (${atMinVersion})`, async () => { + await configureBackend(backend, atMinVersion); + writeAgyAuthFile('antigravity-a.json', { email: 'a@x.com' }); + writeAgyAuthFile('antigravity-b.json', { email: 'b@x.com' }); + + const { registerAccount, setAccountTier, loadDrainOrderConfig } = await loadRegistry(); + registerAccount('agy', 'antigravity-a.json', 'a@x.com'); + registerAccount('agy', 'antigravity-b.json', 'b@x.com'); + setAccountTier('agy', 'a@x.com', 'pro'); + setAccountTier('agy', 'b@x.com', 'free'); + + await runOrderSubcommand(['agy', '--by-tier']); + + expect(process.exitCode).toBe(0); + const output = lines.join('\n'); + expect(output).toContain('Set priorities for 2 account(s).'); + expect('priority' in readAuthFile('antigravity-a.json')).toBe(true); + expect(loadDrainOrderConfig('agy')?.mode).toBe('tier'); + }); + }); + } }); }); diff --git a/src/commands/cliproxy/order-subcommand.ts b/src/commands/cliproxy/order-subcommand.ts index b2b50061..29d3c6cf 100644 --- a/src/commands/cliproxy/order-subcommand.ts +++ b/src/commands/cliproxy/order-subcommand.ts @@ -12,6 +12,7 @@ import { initUI, header, subheader, color, dim, ok, fail, warn, info } from '../ import { extractOption, hasAnyFlag } from '../arg-extractor'; import { saveDrainOrderConfig, clearDrainOrderConfig } from '../../cliproxy/accounts/registry'; import { getProviderAccounts } from '../../cliproxy/accounts/query'; +import { getConfiguredBackend, getInstalledCliproxyVersion } from '../../cliproxy/binary-manager'; import { computeManualDrainOrder, computeTierDrainOrder, @@ -19,6 +20,8 @@ import { resolveEffectiveDrainOrder, clearDrainOrderPriorities, tieBreakKey, + DRAIN_ORDER_MIN_VERSION, + isDrainOrderSupported, type DrainOrderEntry, type DrainOrderInput, } from '../../cliproxy/accounts/drain-order'; @@ -45,6 +48,27 @@ function formatTierLabel(tier: AccountTier | undefined, tierDerived: boolean): s return tierDerived ? label : dim(tier); } +function ensureDrainOrderBinarySupport(): boolean { + const backend = getConfiguredBackend(); + const installedVersion = getInstalledCliproxyVersion(backend); + if (!isDrainOrderSupported(backend, installedVersion)) { + const minimumVersion = DRAIN_ORDER_MIN_VERSION[backend]; + const backendLabel = backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'; + console.log( + fail( + `${backendLabel} v${installedVersion} does not support drain-order priorities (requires v${minimumVersion} or newer).` + ) + ); + console.log( + info(`Run 'ccs cliproxy --latest' to update, then restart with 'ccs cliproxy restart'.`) + ); + console.log(''); + process.exitCode = 1; + return false; + } + return true; +} + function printOrderTable( entries: DrainOrderEntry[], showCurrentPriority: boolean, @@ -281,6 +305,9 @@ async function handleOrderByTier(provider: CLIProxyProvider): Promise { const entries = computeTierDrainOrder(accounts); + if (!ensureDrainOrderBinarySupport()) { + return; + } console.log(subheader('Computed tier-based drain order:')); printOrderTable(entries, false); console.log(''); @@ -404,6 +431,10 @@ async function handleOrderSet(provider: CLIProxyProvider, setArg: string): Promi return; } + if (!ensureDrainOrderBinarySupport()) { + return; + } + console.log(subheader('Computed manual drain order:')); printOrderTable(entries, false); console.log(''); From 3ce7c01d95dbb52ba584a376201d3cad99c0b258 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:29:29 -0400 Subject: [PATCH 5/7] chore(cliproxy): refresh formatting, throw-error baseline, and hardening inventory --- docs/reports/hardening-inventory.json | 2 +- docs/reports/hardening-inventory.md | 2 +- eslint-rules/throw-error-baseline.json | 24 +++++++++---------- src/cliproxy/accounts/drain-order.ts | 5 +--- src/cliproxy/binary/platform-detector.ts | 5 +--- .../__tests__/backend-selection.test.js | 8 +++++-- .../__tests__/routing-strategy.test.ts | 3 ++- .../__tests__/order-subcommand.test.ts | 12 ++++++++-- 8 files changed, 34 insertions(+), 27 deletions(-) diff --git a/docs/reports/hardening-inventory.json b/docs/reports/hardening-inventory.json index 91e33acb..c7a97bbc 100644 --- a/docs/reports/hardening-inventory.json +++ b/docs/reports/hardening-inventory.json @@ -1,7 +1,7 @@ { "scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}", "syncFs": { - "totalOccurrences": 2509, + "totalOccurrences": 2513, "filesAffected": 263, "hotpathOccurrences": 1183, "hotpathFilesAffected": 156, diff --git a/docs/reports/hardening-inventory.md b/docs/reports/hardening-inventory.md index 506b59c8..ab01fc80 100644 --- a/docs/reports/hardening-inventory.md +++ b/docs/reports/hardening-inventory.md @@ -6,7 +6,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | Metric | Value | |---|---:| -| Sync fs occurrences (all) | 2509 | +| Sync fs occurrences (all) | 2513 | | Sync fs files affected (all) | 263 | | Sync fs occurrences (runtime hotpaths) | 1183 | | Sync fs files affected (runtime hotpaths) | 156 | diff --git a/eslint-rules/throw-error-baseline.json b/eslint-rules/throw-error-baseline.json index e1be244b..60d84fa8 100644 --- a/eslint-rules/throw-error-baseline.json +++ b/eslint-rules/throw-error-baseline.json @@ -8,12 +8,12 @@ "src/channels/official-channels-store.ts:291", "src/channels/official-channels-store.ts:296", "src/channels/official-channels-store.ts:299", - "src/cliproxy/accounts/drain-order.ts:131", - "src/cliproxy/accounts/drain-order.ts:139", - "src/cliproxy/accounts/drain-order.ts:246", - "src/cliproxy/accounts/drain-order.ts:253", - "src/cliproxy/accounts/drain-order.ts:279", - "src/cliproxy/accounts/drain-order.ts:342", + "src/cliproxy/accounts/drain-order.ts:149", + "src/cliproxy/accounts/drain-order.ts:157", + "src/cliproxy/accounts/drain-order.ts:264", + "src/cliproxy/accounts/drain-order.ts:271", + "src/cliproxy/accounts/drain-order.ts:297", + "src/cliproxy/accounts/drain-order.ts:360", "src/cliproxy/accounts/registry.ts:521", "src/cliproxy/accounts/registry.ts:536", "src/cliproxy/accounts/registry.ts:544", @@ -38,10 +38,10 @@ "src/cliproxy/binary/installer.ts:74", "src/cliproxy/binary/installer.ts:88", "src/cliproxy/binary/lifecycle.ts:137", - "src/cliproxy/binary/platform-detector.ts:153", - "src/cliproxy/binary/platform-detector.ts:160", + "src/cliproxy/binary/platform-detector.ts:152", + "src/cliproxy/binary/platform-detector.ts:159", "src/cliproxy/binary/verifier.ts:55", - "src/cliproxy/binary/version-checker.ts:108", + "src/cliproxy/binary/version-checker.ts:118", "src/cliproxy/config/base-config-loader.ts:54", "src/cliproxy/config/base-config-loader.ts:66", "src/cliproxy/config/base-config-loader.ts:81", @@ -59,9 +59,9 @@ "src/cliproxy/executor/proxy-resolver.ts:177", "src/cliproxy/executor/session-bridge.ts:147", "src/cliproxy/proxy/https-tunnel-proxy.ts:61", - "src/cliproxy/routing/routing-strategy.ts:480", - "src/cliproxy/routing/routing-strategy.ts:488", - "src/cliproxy/routing/routing-strategy.ts:708", + "src/cliproxy/routing/routing-strategy.ts:490", + "src/cliproxy/routing/routing-strategy.ts:498", + "src/cliproxy/routing/routing-strategy.ts:718", "src/cliproxy/services/remote-auth-fetcher.ts:139", "src/cliproxy/services/remote-auth-fetcher.ts:150", "src/cliproxy/services/remote-auth-fetcher.ts:152", diff --git a/src/cliproxy/accounts/drain-order.ts b/src/cliproxy/accounts/drain-order.ts index b8014818..ec2ee993 100644 --- a/src/cliproxy/accounts/drain-order.ts +++ b/src/cliproxy/accounts/drain-order.ts @@ -40,10 +40,7 @@ export const DRAIN_ORDER_MIN_VERSION: CLIProxyBackendMinVersions = { plus: '6.6.107-0', }; -export function isDrainOrderSupported( - backend: CLIProxyBackend, - installedVersion: string -): boolean { +export function isDrainOrderSupported(backend: CLIProxyBackend, installedVersion: string): boolean { return meetsBackendMinimumVersion(installedVersion, backend, DRAIN_ORDER_MIN_VERSION); } diff --git a/src/cliproxy/binary/platform-detector.ts b/src/cliproxy/binary/platform-detector.ts index 9721afb0..d9e5347b 100644 --- a/src/cliproxy/binary/platform-detector.ts +++ b/src/cliproxy/binary/platform-detector.ts @@ -124,10 +124,7 @@ function getReleaseArchForBackend( publicArch: SupportedArch, releaseArch: SupportedArch ): SupportedArch { - if ( - publicArch === 'arm64' && - !isAtLeastVersion(version, AARCH64_ASSET_MIN_VERSION[backend]) - ) { + if (publicArch === 'arm64' && !isAtLeastVersion(version, AARCH64_ASSET_MIN_VERSION[backend])) { return 'arm64'; } diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index 54bec9a0..40eb3de0 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -95,11 +95,15 @@ describe('Backend Selection', () => { it('selects arm64 vs aarch64 release assets across original and plus boundaries', () => { // Original pre-rename boundaries (<= 6.9.47) use arm64 for (const version of [undefined, '6.9.45', '6.9.47']) { - const effectiveVersion = version || platformDetector.BACKEND_CONFIG.original.fallbackVersion; + const effectiveVersion = + version || platformDetector.BACKEND_CONFIG.original.fallbackVersion; withMockedProcessPlatform('darwin', 'arm64', () => { const info = platformDetector.detectPlatform(version, 'original'); assert.strictEqual(info.arch, 'arm64'); - assert.strictEqual(info.binaryName, `CLIProxyAPI_${effectiveVersion}_darwin_arm64.tar.gz`); + assert.strictEqual( + info.binaryName, + `CLIProxyAPI_${effectiveVersion}_darwin_arm64.tar.gz` + ); assert.strictEqual( platformDetector.getDownloadUrl(version, 'original'), `https://github.com/router-for-me/CLIProxyAPI/releases/download/v${effectiveVersion}/CLIProxyAPI_${effectiveVersion}_darwin_arm64.tar.gz` diff --git a/src/cliproxy/routing/__tests__/routing-strategy.test.ts b/src/cliproxy/routing/__tests__/routing-strategy.test.ts index 271496e0..6a9364c1 100644 --- a/src/cliproxy/routing/__tests__/routing-strategy.test.ts +++ b/src/cliproxy/routing/__tests__/routing-strategy.test.ts @@ -18,7 +18,8 @@ describe('cliproxy routing strategy service', () => { let binaryBackend: CLIProxyBackend = 'original'; let binaryVersion = '7.2.127-7'; let installedVersionQueriedBackend: CLIProxyBackend | undefined = undefined; - let loggedWarnings: Array<{ event: string; message: string; context?: Record }> = []; + let loggedWarnings: Array<{ event: string; message: string; context?: Record }> = + []; let responseFactory: (() => Promise) | null = null; beforeEach(async () => { diff --git a/src/commands/cliproxy/__tests__/order-subcommand.test.ts b/src/commands/cliproxy/__tests__/order-subcommand.test.ts index 37fc0893..90727b07 100644 --- a/src/commands/cliproxy/__tests__/order-subcommand.test.ts +++ b/src/commands/cliproxy/__tests__/order-subcommand.test.ts @@ -19,6 +19,8 @@ import * as path from 'path'; mock.module('../../../cliproxy/proxy/proxy-detector', () => ({ detectRunningProxy: async () => ({ running: false, verified: false }), + waitForProxyHealthy: async () => ({ running: false, verified: false }), + reclaimOrphanedProxy: () => {}, })); describe('handleOrderSubcommand', () => { @@ -199,7 +201,7 @@ describe('handleOrderSubcommand', () => { expect(output).toContain('reset to file order'); expect(output).toContain('no priority set'); }); -}); + }); describe('binary version capability gate (#1724)', () => { const cases: Array<{ @@ -225,7 +227,13 @@ describe('handleOrderSubcommand', () => { }, ]; - for (const { backend, belowMinVersion, atMinVersion, backendLabel, requiredMinVersion } of cases) { + for (const { + backend, + belowMinVersion, + atMinVersion, + backendLabel, + requiredMinVersion, + } of cases) { describe(`${backend} backend`, () => { it(`refuses --set below minimum version (${belowMinVersion}) without mutating files`, async () => { await configureBackend(backend, belowMinVersion); From fcbac629d277c9fc65f81817ca70108fdc88c062 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 2 Sep 2026 11:35:00 -0400 Subject: [PATCH 6/7] test(cliproxy): strengthen fallback download url and ungated show/reset coverage --- .../__tests__/backend-selection.test.js | 26 ++++++++++++++----- .../__tests__/order-subcommand.test.ts | 16 +++++++++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/cliproxy/config/__tests__/backend-selection.test.js b/src/cliproxy/config/__tests__/backend-selection.test.js index 40eb3de0..116b3ddb 100644 --- a/src/cliproxy/config/__tests__/backend-selection.test.js +++ b/src/cliproxy/config/__tests__/backend-selection.test.js @@ -142,18 +142,32 @@ describe('Backend Selection', () => { ); }); - // Plus fallback / pre-rename (< 6.10.0-0) uses arm64 - withMockedProcessPlatform('darwin', 'arm64', () => { - const info = platformDetector.detectPlatform('6.9.45-0', 'plus'); - assert.strictEqual(info.arch, 'arm64'); - assert.strictEqual(info.binaryName, 'CLIProxyAPIPlus_6.9.45-0_darwin_arm64.tar.gz'); - }); + // Plus fallback (undefined) and explicit pre-rename (< 6.10.0-0) use arm64 + for (const version of [undefined, '6.9.45-0']) { + const effectiveVersion = version || platformDetector.BACKEND_CONFIG.plus.fallbackVersion; + withMockedProcessPlatform('darwin', 'arm64', () => { + const info = platformDetector.detectPlatform(version, 'plus'); + assert.strictEqual(info.arch, 'arm64'); + assert.strictEqual( + info.binaryName, + `CLIProxyAPIPlus_${effectiveVersion}_darwin_arm64.tar.gz` + ); + assert.strictEqual( + platformDetector.getDownloadUrl(version, 'plus'), + `https://github.com/kaitranntt/CLIProxyAPIPlus/releases/download/v${effectiveVersion}/CLIProxyAPIPlus_${effectiveVersion}_darwin_arm64.tar.gz` + ); + }); + } // Plus post-rename (>= 6.10.0-0) uses aarch64 withMockedProcessPlatform('darwin', 'arm64', () => { const info = platformDetector.detectPlatform('6.10.0-0', 'plus'); assert.strictEqual(info.arch, 'arm64'); assert.strictEqual(info.binaryName, 'CLIProxyAPIPlus_6.10.0-0_darwin_aarch64.tar.gz'); + assert.strictEqual( + platformDetector.getDownloadUrl('6.10.0-0', 'plus'), + 'https://github.com/kaitranntt/CLIProxyAPIPlus/releases/download/v6.10.0-0/CLIProxyAPIPlus_6.10.0-0_darwin_aarch64.tar.gz' + ); }); // x64 remains amd64 for both backends diff --git a/src/commands/cliproxy/__tests__/order-subcommand.test.ts b/src/commands/cliproxy/__tests__/order-subcommand.test.ts index 90727b07..4ce07eb0 100644 --- a/src/commands/cliproxy/__tests__/order-subcommand.test.ts +++ b/src/commands/cliproxy/__tests__/order-subcommand.test.ts @@ -122,9 +122,8 @@ describe('handleOrderSubcommand', () => { describe('file-mode show with residual priorities', () => { it('renders selector pick order (priority desc) and flags drift instead of alphabetical order', async () => { - // Residual on-disk priorities, no stored config -> file mode + drift. - // claude-a sorts first alphabetically, but b has the higher priority, so - // the selector drains b first. The display must follow the selector. + // Explicit below-minimum version to prove show is ungated on old binaries. + await configureBackend('original', '6.6.105'); writeAuthFile('claude-a.json', { email: 'a@x.com', priority: 1 }); writeAuthFile('claude-b.json', { email: 'b@x.com', priority: 5 }); @@ -133,6 +132,7 @@ describe('handleOrderSubcommand', () => { registerAccount('claude', 'claude-b.json', 'b@x.com'); await runOrderSubcommand(['claude']); + expect(process.exitCode).toBe(0); const output = lines.join('\n'); // b@x.com (priority 5) must appear before a@x.com (priority 1). @@ -142,7 +142,6 @@ describe('handleOrderSubcommand', () => { expect(idxA).toBeGreaterThan(idxB); // Drift surfaced (same as the manual/tier branch), and the mode label no - // longer falsely claims "no priority set" under residual priorities. expect(output).toContain('Drift detected'); expect(output).toContain('residual priorities present'); expect(output).not.toContain('no priority set'); @@ -152,6 +151,8 @@ describe('handleOrderSubcommand', () => { }); it('keeps the plain "no priority set" label and no drift when there are no residuals', async () => { + // Explicit below-minimum version on Plus to prove show is ungated on old binaries. + await configureBackend('plus', '6.6.105-0'); writeAuthFile('claude-a.json', { email: 'a@x.com' }); writeAuthFile('claude-b.json', { email: 'b@x.com' }); @@ -160,6 +161,7 @@ describe('handleOrderSubcommand', () => { registerAccount('claude', 'claude-b.json', 'b@x.com'); await runOrderSubcommand(['claude']); + expect(process.exitCode).toBe(0); const output = lines.join('\n'); expect(output).toContain('no priority set'); @@ -169,6 +171,8 @@ describe('handleOrderSubcommand', () => { describe('--reset clears residual priorities (proxy stopped -> direct write)', () => { it('removes the priority field from auth files and reports per-file results', async () => { + // Explicit below-minimum version to prove reset is ungated on old binaries. + await configureBackend('original', '6.6.105'); writeAuthFile('claude-a.json', { email: 'a@x.com', priority: 4 }); writeAuthFile('claude-b.json', { email: 'b@x.com' }); // already clear @@ -178,6 +182,7 @@ describe('handleOrderSubcommand', () => { saveDrainOrderConfig('claude', { mode: 'manual', orderedIds: ['a@x.com', 'b@x.com'] }); await runOrderSubcommand(['claude', '--reset']); + expect(process.exitCode).toBe(0); // Residual priority is actually gone from disk (not just config deleted). expect('priority' in readAuthFile('claude-a.json')).toBe(false); @@ -190,12 +195,15 @@ describe('handleOrderSubcommand', () => { }); it('reports already-clear files and still resets when no priorities exist', async () => { + // Explicit below-minimum version on Plus to prove reset is ungated on old binaries. + await configureBackend('plus', '6.6.105-0'); writeAuthFile('claude-a.json', { email: 'a@x.com' }); const { registerAccount } = await registerClaude(); registerAccount('claude', 'claude-a.json', 'a@x.com'); await runOrderSubcommand(['claude', '--reset']); + expect(process.exitCode).toBe(0); const output = lines.join('\n'); expect(output).toContain('reset to file order'); From 8cb3451f8153af3be6b855f3651d925efeb4e1de Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 9 Sep 2026 15:24:08 -0600 Subject: [PATCH 7/7] docs: refresh hardening inventory after dev merge --- docs/reports/hardening-inventory.json | 2 +- docs/reports/hardening-inventory.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reports/hardening-inventory.json b/docs/reports/hardening-inventory.json index b89917eb..545f40c6 100644 --- a/docs/reports/hardening-inventory.json +++ b/docs/reports/hardening-inventory.json @@ -779,7 +779,7 @@ }, { "file": "src/cliproxy/model-catalog.ts", - "loc": 884 + "loc": 895 } ] } diff --git a/docs/reports/hardening-inventory.md b/docs/reports/hardening-inventory.md index 95856df3..a8aa1818 100644 --- a/docs/reports/hardening-inventory.md +++ b/docs/reports/hardening-inventory.md @@ -103,5 +103,5 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}` | `src/cliproxy/quota/quota-manager.ts` | 954 | | `src/web-server/services/codex-dashboard-service.ts` | 940 | | `src/glmt/glmt-proxy.ts` | 939 | -| `src/cliproxy/model-catalog.ts` | 884 | +| `src/cliproxy/model-catalog.ts` | 895 |