fix: stabilize validate pipeline and cliproxy route tests

This commit is contained in:
Tam Nhu Tran
2026-03-20 09:29:51 -04:00
parent e2ca9c2407
commit 0d90f04f3e
22 changed files with 214 additions and 65 deletions
+2 -2
View File
@@ -71,8 +71,8 @@
"verify:bundle": "node scripts/verify-bundle.js",
"test": "bun run build && bun run test:all",
"test:ci": "bun run test:all",
"test:all": "bun test tests/unit tests/integration tests/npm",
"test:unit": "bun test tests/unit/",
"test:all": "node scripts/run-tests-serial.js tests/unit tests/integration tests/npm",
"test:unit": "node scripts/run-tests-serial.js tests/unit",
"test:npm": "bun test tests/npm/",
"test:native": "bash tests/native/unix/edge-cases.sh",
"test:e2e": "bun test tests/e2e/ --bail --timeout 60000",
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const roots = process.argv.slice(2);
const testRoots = roots.length > 0 ? roots : ['tests'];
const testFilePattern = /(?:\.test|_test_|\.spec|_spec_)\.(?:[cm]?[jt]s)$/;
function collectTestFiles(rootDir) {
if (!fs.existsSync(rootDir)) {
return [];
}
const stats = fs.statSync(rootDir);
if (stats.isFile()) {
return testFilePattern.test(path.basename(rootDir)) ? [rootDir] : [];
}
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(rootDir, entry.name);
if (entry.isDirectory()) {
files.push(...collectTestFiles(fullPath));
continue;
}
if (!entry.isFile()) {
continue;
}
if (testFilePattern.test(entry.name)) {
files.push(fullPath);
}
}
return files;
}
const testFiles = [...new Set(testRoots.flatMap((rootDir) => collectTestFiles(rootDir)))].sort();
if (testFiles.length === 0) {
console.error('No test files found.');
process.exit(1);
}
for (let index = 0; index < testFiles.length; index += 1) {
const file = testFiles[index];
console.log(`\n[${index + 1}/${testFiles.length}] ${file}`);
const bunTestPath = path.isAbsolute(file) ? file : `./${file}`;
const result = spawnSync('bun', ['test', bunTestPath], {
stdio: 'inherit',
env: process.env,
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderModelMapping } from './types';
import type { CLIProxyProvider, ProviderModelMapping } from './types';
/** Base settings file structure */
interface BaseSettings {
+2 -1
View File
@@ -3,7 +3,8 @@
* Used by API routes, service layer, and config loader to avoid contract drift.
*/
import { CLIPROXY_SUPPORTED_PROVIDERS, CompositeTierConfig } from '../config/unified-config-types';
import { CLIPROXY_SUPPORTED_PROVIDERS } from '../config/unified-config-types';
import type { CompositeTierConfig } from '../config/unified-config-types';
import type { CLIProxyProvider } from './types';
import { getDeniedModelIdReasonForProvider } from './model-id-normalizer';
+2 -2
View File
@@ -5,13 +5,13 @@
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderModelMapping } from '../types';
import type { CLIProxyProvider, ProviderModelMapping } from '../types';
import { getModelMappingFromConfig, getEnvVarsFromConfig } from '../base-config-loader';
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
import { getEffectiveApiKey } from '../auth-token-manager';
import { expandPath } from '../../utils/helpers';
import { warn } from '../../utils/ui';
import { CompositeTierConfig } from '../../config/unified-config-types';
import type { CompositeTierConfig } from '../../config/unified-config-types';
import {
validatePort,
validateRemotePort,
@@ -9,7 +9,7 @@
* - Claude (Anthropic): Opt-in via --1m flag
*/
import { CLIProxyProvider } from '../types';
import type { CLIProxyProvider } from '../types';
import { supportsExtendedContext } from '../model-catalog';
import { warn } from '../../utils/ui';
import {
+1 -1
View File
@@ -5,7 +5,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { CLIProxyProvider, ProviderConfig } from '../types';
import type { CLIProxyProvider, ProviderConfig } from '../types';
import { getProviderDisplayName } from '../provider-capabilities';
import { getModelMappingFromConfig } from '../base-config-loader';
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
+1 -1
View File
@@ -5,7 +5,7 @@
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import { CLIProxyProvider } from '../types';
import type { CLIProxyProvider } from '../types';
import { CLIPROXY_DEFAULT_PORT } from './port-manager';
/**
+3 -2
View File
@@ -3,8 +3,9 @@
* Manages thinking budget suffixes for CLIProxyAPIPlus
*/
import { CLIProxyProvider } from '../types';
import { ThinkingConfig, DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types';
import type { CLIProxyProvider } from '../types';
import { DEFAULT_THINKING_TIER_DEFAULTS } from '../../config/unified-config-types';
import type { ThinkingConfig } from '../../config/unified-config-types';
import { getThinkingConfig } from '../../config/unified-config-loader';
import { supportsThinking } from '../model-catalog';
import { isThinkingOffValue, validateThinking } from '../thinking-validator';
+1 -1
View File
@@ -5,7 +5,7 @@
* Models are mapped to their internal names used by the proxy backend.
*/
import { CLIProxyProvider } from './types';
import type { CLIProxyProvider } from './types';
import {
isAntigravityProvider,
migrateDeniedAntigravityModelAliases,
+1 -1
View File
@@ -5,7 +5,7 @@
* model version formats (e.g., 4.6 vs 4-6).
*/
import { CLIProxyProvider } from './types';
import type { CLIProxyProvider } from './types';
/** Env vars that carry model identifiers. */
export const MODEL_ENV_VAR_KEYS = [
+3 -2
View File
@@ -7,8 +7,9 @@
* - Warns when model doesn't support thinking
*/
import { getModelThinkingSupport, ThinkingSupport } from './model-catalog';
import { CLIProxyProvider } from './types';
import { getModelThinkingSupport } from './model-catalog';
import type { ThinkingSupport } from './model-catalog';
import type { CLIProxyProvider } from './types';
/**
* Thinking budget bounds (used for validation across API and CLI)
+2 -2
View File
@@ -1,7 +1,7 @@
import * as os from 'os';
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
const WILDCARD_HOSTS = new Set(['0.0.0.0', '::', '[::]']);
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
const WILDCARD_HOSTS = new Set(['0.0.0.0', '::']);
interface NetworkInterfaceCandidate {
address: string;
+3 -1
View File
@@ -10,7 +10,6 @@ import * as path from 'path';
import * as yaml from 'js-yaml';
import { getCcsDir } from '../utils/config-manager';
import {
UnifiedConfig,
isUnifiedConfig,
createEmptyUnifiedConfig,
UNIFIED_CONFIG_VERSION,
@@ -23,6 +22,9 @@ import {
DEFAULT_THINKING_CONFIG,
DEFAULT_DASHBOARD_AUTH_CONFIG,
DEFAULT_IMAGE_ANALYSIS_CONFIG,
} from './unified-config-types';
import type {
UnifiedConfig,
CLIProxySafetyConfig,
GlobalEnvConfig,
ThinkingConfig,
+11 -9
View File
@@ -1,4 +1,4 @@
import { SpawnOptions as NodeSpawnOptions } from 'child_process';
import type { SpawnOptions as NodeSpawnOptions } from 'child_process';
/**
* CLI Runtime Types
@@ -43,11 +43,13 @@ export interface ClaudeCliInfo {
/**
* Exit codes
*/
export enum ExitCode {
SUCCESS = 0,
GENERIC_ERROR = 1,
CLAUDE_NOT_FOUND = 127,
CONFIG_ERROR = 2,
DELEGATION_ERROR = 3,
TIMEOUT = 124,
}
export const ExitCode = {
SUCCESS: 0,
GENERIC_ERROR: 1,
CLAUDE_NOT_FOUND: 127,
CONFIG_ERROR: 2,
DELEGATION_ERROR: 3,
TIMEOUT: 124,
} as const;
export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
+8 -6
View File
@@ -9,12 +9,14 @@ export type { ErrorCode } from '../utils/error-codes';
/**
* Log levels
*/
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
export const LogLevel = {
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
ERROR: 'error',
} as const;
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
/**
* Color codes (TTY-aware)
+2 -8
View File
@@ -1,14 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
Config,
isConfig,
Settings,
isSettings,
CLIProxyVariantsConfig,
CLIProxyVariantConfig,
} from '../types';
import { isConfig, isSettings } from '../types';
import type { Config, Settings, CLIProxyVariantsConfig, CLIProxyVariantConfig } from '../types';
import { expandPath, error } from './helpers';
import { info } from './ui';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
+31 -5
View File
@@ -8,7 +8,7 @@
import { Router, Request, Response } from 'express';
import ProfileRegistry from '../../auth/profile-registry';
import InstanceManager from '../../management/instance-manager';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import {
getAllAccountsSummary,
setDefaultAccount as setCliproxyDefault,
@@ -30,12 +30,28 @@ import {
parseCliproxyKey,
type MergedAccountEntry,
} from './account-route-helpers';
import type { AccountConfig } from '../../config/unified-config-types';
const router = Router();
const registry = new ProfileRegistry();
const instanceMgr = new InstanceManager();
function createProfileRegistry(): ProfileRegistry {
return new ProfileRegistry();
}
function createInstanceManager(): InstanceManager {
return new InstanceManager();
}
function getUnifiedAccountsRaw(): Record<string, AccountConfig> {
if (!isUnifiedMode()) {
return {};
}
return loadOrCreateUnifiedConfig().accounts;
}
function hasAuthAccount(name: string): boolean {
const registry = createProfileRegistry();
return registry.hasAccountUnified(name) || registry.hasProfile(name);
}
@@ -44,8 +60,11 @@ function hasAuthAccount(name: string): boolean {
*/
router.get('/', (_req: Request, res: Response): void => {
try {
const registry = createProfileRegistry();
// Get profiles from both legacy and unified config (same logic as CLI)
const legacyProfiles = registry.getAllProfiles();
const rawUnifiedAccounts = getUnifiedAccountsRaw();
const unifiedAccounts = registry.getAllAccountsUnified();
// Get CLIProxy OAuth accounts (gemini, codex, agy, etc.)
@@ -76,11 +95,12 @@ router.get('/', (_req: Request, res: Response): void => {
// Override with unified config accounts (takes precedence)
for (const [name, account] of Object.entries(unifiedAccounts)) {
const rawAccount = rawUnifiedAccounts[name];
const contextPolicy = resolveAccountContextPolicy(account);
const hasExplicitContextMode =
account.context_mode === 'isolated' || account.context_mode === 'shared';
rawAccount?.context_mode === 'isolated' || rawAccount?.context_mode === 'shared';
const hasExplicitContinuityMode =
account.continuity_mode === 'standard' || account.continuity_mode === 'deeper';
rawAccount?.continuity_mode === 'standard' || rawAccount?.continuity_mode === 'deeper';
merged[name] = {
type: 'account',
created: account.created,
@@ -138,6 +158,7 @@ router.get('/', (_req: Request, res: Response): void => {
*/
router.post('/default', (req: Request, res: Response): void => {
try {
const registry = createProfileRegistry();
const { name } = req.body;
if (!name) {
@@ -175,6 +196,8 @@ router.post('/default', (req: Request, res: Response): void => {
*/
router.put('/:name/context', async (req: Request, res: Response): Promise<void> => {
try {
const registry = createProfileRegistry();
const instanceMgr = createInstanceManager();
const { name } = req.params;
if (!name) {
@@ -310,6 +333,7 @@ router.put('/:name/context', async (req: Request, res: Response): Promise<void>
*/
router.delete('/reset-default', (_req: Request, res: Response): void => {
try {
const registry = createProfileRegistry();
if (isUnifiedMode()) {
registry.clearDefaultUnified();
} else {
@@ -326,6 +350,8 @@ router.delete('/reset-default', (_req: Request, res: Response): void => {
*/
router.delete('/:name', async (req: Request, res: Response): Promise<void> => {
try {
const registry = createProfileRegistry();
const instanceMgr = createInstanceManager();
const { name } = req.params;
if (!name) {
@@ -90,6 +90,6 @@ describe('buildClaudeEnvironment - composite remote routing', () => {
expect(env.ANTHROPIC_MODEL).toMatch(/^claude-opus-4-6-thinking(\([^)]+\))?$/);
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toMatch(/^claude-opus-4-6-thinking(\([^)]+\))?$/);
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toMatch(/^gemini-2.5-pro(\([^)]+\))?$/);
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('gpt-5.1-codex-mini');
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toMatch(/^gpt-5.1-codex-mini(?:-medium)?$/);
});
});
@@ -14,18 +14,30 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
import claudeExtensionRoutes from '../../../src/web-server/routes/claude-extension-routes';
import SharedManager from '../../../src/management/shared-manager';
import { createEmptyUnifiedConfig } from '../../../src/config/unified-config-types';
import { saveUnifiedConfig } from '../../../src/config/unified-config-loader';
describe('web-server claude-extension-routes', () => {
let server: Server;
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
let originalClaudeConfigDir: string | undefined;
let setGlobalConfigDir: (dir: string | undefined) => void;
let claudeExtensionRoutes: ReturnType<typeof express.Router>;
let SharedManager: typeof import('../../../src/management/shared-manager').default;
let createEmptyUnifiedConfig: typeof import('../../../src/config/unified-config-types').createEmptyUnifiedConfig;
let saveUnifiedConfig: typeof import('../../../src/config/unified-config-loader').saveUnifiedConfig;
beforeAll(async () => {
originalCcsHome = process.env.CCS_HOME;
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
({ setGlobalConfigDir } = await import('../../../src/utils/config-manager'));
({ default: SharedManager } = await import('../../../src/management/shared-manager'));
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
({ default: claudeExtensionRoutes } = await import(
'../../../src/web-server/routes/claude-extension-routes'
));
const app = express();
app.use(express.json());
app.use('/api/claude-extension', claudeExtensionRoutes);
@@ -49,12 +61,20 @@ describe('web-server claude-extension-routes', () => {
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
setGlobalConfigDir(undefined);
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
if (originalClaudeConfigDir !== undefined) process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
else delete process.env.CLAUDE_CONFIG_DIR;
});
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-claude-extension-routes-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
process.env.CLAUDE_CONFIG_DIR = path.join(tempHome, '.claude');
setGlobalConfigDir(path.join(tempHome, '.ccs'));
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(ccsDir, { recursive: true });
@@ -110,9 +130,9 @@ describe('web-server claude-extension-routes', () => {
afterEach(() => {
mock.restore();
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
setGlobalConfigDir(undefined);
delete process.env.CCS_HOME;
delete process.env.CLAUDE_CONFIG_DIR;
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
@@ -7,12 +7,34 @@ const installSpy = {
};
mock.module('../../../src/config/unified-config-loader', () => ({
isUnifiedMode: () => true,
loadUnifiedConfig: () => ({
cliproxy: { backend: 'plus' },
}),
loadOrCreateUnifiedConfig: () => ({
cliproxy: { backend: 'plus' },
}),
getGlobalEnvConfig: () => ({}),
getThinkingConfig: () => ({
mode: 'auto',
tier_defaults: {
opus: 'high',
sonnet: 'high',
haiku: 'medium',
},
provider_overrides: {},
show_warnings: true,
}),
getCliproxySafetyConfig: () => ({
antigravity_ack_bypass: false,
}),
saveUnifiedConfig: () => {},
}));
mock.module('../../../src/cliproxy/binary-manager', () => ({
ensureCLIProxyBinary: async () => '/tmp/cliproxy',
isCLIProxyInstalled: () => true,
getCLIProxyPath: () => '/tmp/cliproxy',
checkCliproxyUpdate: async () => ({
hasUpdate: false,
currentVersion: '6.6.80',
@@ -26,6 +48,7 @@ mock.module('../../../src/cliproxy/binary-manager', () => ({
}),
getInstalledCliproxyVersion: () => '6.6.80',
installCliproxyVersion: async () => {},
fetchLatestCliproxyVersion: async () => '6.6.89',
}));
mock.module('../../../src/cliproxy/binary/version-checker', () => ({
@@ -103,7 +126,9 @@ beforeEach(() => {
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
mock.restore();
});
@@ -4,7 +4,6 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
import cliproxyStatsRoutes from '../../../src/web-server/routes/cliproxy-stats-routes';
function writeSettings(filePath: string, env: Record<string, string>): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -16,8 +15,16 @@ describe('cliproxy-stats-routes model update canonicalization', () => {
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
let setGlobalConfigDir: (dir: string | undefined) => void;
let cliproxyStatsRoutes: ReturnType<typeof express.Router>;
beforeAll(async () => {
originalCcsHome = process.env.CCS_HOME;
({ setGlobalConfigDir } = await import('../../../src/utils/config-manager'));
({ default: cliproxyStatsRoutes } = await import(
'../../../src/web-server/routes/cliproxy-stats-routes'
));
const app = express();
app.use(express.json());
app.use('/api/cliproxy', cliproxyStatsRoutes);
@@ -41,20 +48,24 @@ describe('cliproxy-stats-routes model update canonicalization', () => {
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
});
setGlobalConfigDir(undefined);
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-model-route-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
});
beforeEach(() => {
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-model-route-'));
process.env.CCS_HOME = tempHome;
setGlobalConfigDir(path.join(tempHome, '.ccs'));
});
afterEach(() => {
setGlobalConfigDir(undefined);
delete process.env.CCS_HOME;
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });