fix: restore parallel-safe validate pipeline

This commit is contained in:
Tam Nhu Tran
2026-03-20 09:51:41 -04:00
parent 0d90f04f3e
commit 04fd8ff019
11 changed files with 176 additions and 306 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": "node scripts/run-tests-serial.js tests/unit tests/integration tests/npm",
"test:unit": "node scripts/run-tests-serial.js tests/unit",
"test:all": "bun test tests/unit tests/integration tests/npm",
"test:unit": "bun test 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
@@ -1,64 +0,0 @@
#!/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);
}
}
+2 -6
View File
@@ -1,5 +1,5 @@
import * as os from 'os';
import * as path from 'path';
import { getCcsHome } from './config-manager';
/**
* Resolve Claude config directory with test/dev overrides.
@@ -13,11 +13,7 @@ export function getClaudeConfigDir(): string {
return path.resolve(process.env.CLAUDE_CONFIG_DIR);
}
if (process.env.CCS_HOME) {
return path.join(path.resolve(process.env.CCS_HOME), '.claude');
}
return path.join(os.homedir(), '.claude');
return path.join(getCcsHome(), '.claude');
}
/** Resolve Claude settings.json path. */
+41
View File
@@ -1,3 +1,4 @@
import { AsyncLocalStorage } from 'async_hooks';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -13,6 +14,38 @@ import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-conf
// Module-level state for --config-dir CLI flag override
let _globalConfigDir: string | undefined;
const configScopeStorage = new AsyncLocalStorage<{ ccsHome?: string; ccsDir?: string }>();
function normalizeScopedPath(dir: string | undefined): string | undefined {
return dir ? path.resolve(dir) : undefined;
}
export async function runWithScopedConfig<T>(
scope: { ccsHome?: string; ccsDir?: string },
fn: () => Promise<T> | T
): Promise<T> {
return await configScopeStorage.run(
{
ccsHome: normalizeScopedPath(scope.ccsHome),
ccsDir: normalizeScopedPath(scope.ccsDir),
},
fn
);
}
export async function runWithScopedCcsHome<T>(
ccsHome: string,
fn: () => Promise<T> | T
): Promise<T> {
return await runWithScopedConfig({ ccsHome }, fn);
}
export async function runWithScopedConfigDir<T>(
ccsDir: string,
fn: () => Promise<T> | T
): Promise<T> {
return await runWithScopedConfig({ ccsDir }, fn);
}
/**
* Set global config directory from --config-dir CLI flag.
@@ -28,6 +61,10 @@ export function setGlobalConfigDir(dir: string | undefined): void {
* @returns Home directory path
*/
export function getCcsHome(): string {
const scopedHome = configScopeStorage.getStore()?.ccsHome;
if (scopedHome) {
return scopedHome;
}
return process.env.CCS_HOME || os.homedir();
}
@@ -36,6 +73,10 @@ export function getCcsHome(): string {
* Single source of truth for precedence logic.
*/
function _resolveCcsDir(): { source: string; dir: string } {
const scopedConfig = configScopeStorage.getStore();
if (scopedConfig?.ccsDir) return { source: 'scoped:CCS_DIR', dir: scopedConfig.ccsDir };
if (scopedConfig?.ccsHome)
return { source: 'scoped:CCS_HOME', dir: path.join(scopedConfig.ccsHome, '.ccs') };
if (_globalConfigDir) return { source: '--config-dir', dir: _globalConfigDir };
if (process.env.CCS_DIR) return { source: 'CCS_DIR', dir: path.resolve(process.env.CCS_DIR) };
if (process.env.CCS_HOME)
@@ -2,8 +2,10 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { clearQuotaCache } from '../../../src/cliproxy/quota-response-cache';
afterEach(() => {
clearQuotaCache();
mock.restore();
});
@@ -57,10 +59,6 @@ describe('codex plan compatibility reconcile', () => {
accountId: 'free@example.com',
}),
}));
mock.module('../../../src/cliproxy/quota-response-cache', () => ({
getCachedQuota: () => null,
setCachedQuota: () => {},
}));
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
@@ -103,10 +101,6 @@ describe('codex plan compatibility reconcile', () => {
throw new Error('should not fetch quota without a default account');
},
}));
mock.module('../../../src/cliproxy/quota-response-cache', () => ({
getCachedQuota: () => null,
setCachedQuota: () => {},
}));
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
@@ -153,10 +147,6 @@ describe('codex plan compatibility reconcile', () => {
accountId: `${planType}@example.com`,
}),
}));
mock.module('../../../src/cliproxy/quota-response-cache', () => ({
getCachedQuota: () => null,
setCachedQuota: () => {},
}));
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
@@ -202,10 +192,6 @@ describe('codex plan compatibility reconcile', () => {
error: 'network timeout',
}),
}));
mock.module('../../../src/cliproxy/quota-response-cache', () => ({
getCachedQuota: () => null,
setCachedQuota: () => {},
}));
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
@@ -250,10 +236,6 @@ describe('codex plan compatibility reconcile', () => {
accountId: 'missing-plan@example.com',
}),
}));
mock.module('../../../src/cliproxy/quota-response-cache', () => ({
getCachedQuota: () => null,
setCachedQuota: () => {},
}));
mock.module('../../../src/utils/ui', () => ({
info: (message: string) => message,
warn: (message: string) => message,
+2 -47
View File
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
const startServerCalls: Array<Record<string, unknown>> = [];
const resolveDashboardUrlsCalls: Array<[string | undefined, number]> = [];
const configAuthCalls: string[][] = [];
let logLines: string[] = [];
let errorLines: string[] = [];
@@ -14,7 +13,6 @@ let originalProcessExit: typeof process.exit;
beforeEach(() => {
startServerCalls.length = 0;
resolveDashboardUrlsCalls.length = 0;
configAuthCalls.length = 0;
logLines = [];
errorLines = [];
@@ -91,42 +89,6 @@ beforeEach(() => {
mock.module('../../../src/utils/ui', () => uiModule);
mock.module('../../../src/utils/ui.ts', () => uiModule);
mock.module('../../../src/commands/config-dashboard-host', () => ({
normalizeDashboardHost: (host: string | undefined) => {
if (!host) {
return undefined;
}
if (host.startsWith('[') && host.endsWith(']') && host.includes(':')) {
return host.slice(1, -1);
}
return host;
},
isLoopbackHost: (host: string) =>
['localhost', '127.0.0.1', '::1', '[::1]'].includes(host.trim().toLowerCase()),
isWildcardHost: (host: string) => ['0.0.0.0', '::', '[::]'].includes(host.trim().toLowerCase()),
resolveDashboardUrls: (host: string | undefined, port: number) => {
resolveDashboardUrlsCalls.push([host, port]);
if (!host) {
return { browserUrl: `http://localhost:${port}` };
}
if (host === '0.0.0.0' || host === '::') {
return {
bindHost: host,
browserUrl: `http://localhost:${port}`,
networkUrls: [`http://192.168.1.25:${port}`, `http://100.64.0.12:${port}`],
};
}
return {
bindHost: host,
browserUrl: `http://${host}:${port}`,
};
},
}));
mock.module('../../../src/commands/config-auth', () => ({
handleConfigAuthCommand: async (args: string[]) => {
configAuthCalls.push([...args]);
@@ -158,7 +120,6 @@ describe('config command dashboard startup', () => {
await expect(handleConfigCommand(['help'])).rejects.toThrow('process.exit(0)');
expect(startServerCalls).toHaveLength(0);
expect(resolveDashboardUrlsCalls).toHaveLength(0);
expect(logLines.join('\n')).toContain('Usage: ccs config [command] [options]');
});
@@ -169,7 +130,6 @@ describe('config command dashboard startup', () => {
expect(configAuthCalls).toEqual([['setup']]);
expect(startServerCalls).toHaveLength(0);
expect(resolveDashboardUrlsCalls).toHaveLength(0);
});
it('rejects unknown config subcommands before dashboard startup', async () => {
@@ -181,7 +141,6 @@ describe('config command dashboard startup', () => {
await expect(handleConfigCommand(['bogus'])).rejects.toThrow('process.exit(1)');
expect(startServerCalls).toHaveLength(0);
expect(resolveDashboardUrlsCalls).toHaveLength(0);
expect(errorLines.join('\n')).toContain('Unexpected arguments: bogus');
});
@@ -192,12 +151,11 @@ describe('config command dashboard startup', () => {
expect(startServerCalls).toHaveLength(1);
expect(startServerCalls[0]).toEqual({ port: 3000, dev: false });
expect(resolveDashboardUrlsCalls).toEqual([['::', 3000]]);
const rendered = logLines.join('\n');
expect(rendered).toContain('Dashboard: http://localhost:3000');
expect(rendered).toContain('Bind host: ::');
expect(rendered).toContain('Network URLs:');
expect(rendered).toContain('Dashboard may be reachable from other devices that can connect to this machine.');
expect(rendered).toContain('Protect it before sharing: ccs config auth setup');
expect(errorLines).toHaveLength(0);
});
@@ -210,13 +168,10 @@ describe('config command dashboard startup', () => {
expect(startServerCalls).toHaveLength(1);
expect(startServerCalls[0]).toEqual({ port: 4100, dev: false, host: '0.0.0.0' });
expect(resolveDashboardUrlsCalls).toEqual([['0.0.0.0', 4100]]);
const rendered = logLines.join('\n');
expect(rendered).toContain('Dashboard: http://localhost:4100');
expect(rendered).toContain('Bind host: 0.0.0.0');
expect(rendered).toContain('Network URLs:');
expect(rendered).toContain('http://192.168.1.25:4100');
expect(rendered).toContain('http://100.64.0.12:4100');
expect(rendered).toContain(
'Dashboard may be reachable from other devices that can connect to this machine.'
);
@@ -6,6 +6,7 @@ import * as lockfile from 'proper-lockfile';
import { handlePersistCommand } from '../../../src/commands/persist-command';
import { createEmptyUnifiedConfig } from '../../../src/config/unified-config-types';
import { saveUnifiedConfig } from '../../../src/config/unified-config-loader';
import { runWithScopedCcsHome } from '../../../src/utils/config-manager';
interface RestoreFixture {
claudeDir: string;
@@ -18,11 +19,14 @@ interface RestoreFixture {
let tempRoot: string;
let originalClaudeConfigDir: string | undefined;
let originalCcsHome: string | undefined;
let originalProcessExit: typeof process.exit;
let originalFsOpen: typeof fs.promises.open;
let originalFsRename: typeof fs.promises.rename;
async function withScopedHome<T>(fn: () => Promise<T>): Promise<T> {
return await runWithScopedCcsHome(tempRoot, fn);
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
@@ -69,11 +73,9 @@ function stubProcessExit(): void {
beforeEach(async () => {
tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ccs-persist-handler-test-'));
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
originalCcsHome = process.env.CCS_HOME;
originalProcessExit = process.exit;
originalFsOpen = fs.promises.open;
originalFsRename = fs.promises.rename;
process.env.CCS_HOME = tempRoot;
});
afterEach(async () => {
@@ -87,12 +89,6 @@ afterEach(async () => {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
}
if (originalCcsHome === undefined) {
delete process.env.CCS_HOME;
} else {
process.env.CCS_HOME = originalCcsHome;
}
if (tempRoot) {
await fs.promises.rm(tempRoot, { recursive: true, force: true });
}
@@ -100,50 +96,50 @@ afterEach(async () => {
describe('persist command real handler paths', () => {
it('throws parseError for missing --permission-mode before profile detection', async () => {
await expect(handlePersistCommand(['glm', '--permission-mode'])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode']))).rejects.toThrow(
'Missing value for --permission-mode'
);
});
it('throws parseError for empty inline --permission-mode before profile detection', async () => {
await expect(handlePersistCommand(['glm', '--permission-mode='])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
'Missing value for --permission-mode'
);
});
it('throws parseError for invalid --permission-mode before profile detection', async () => {
await expect(handlePersistCommand(['glm', '--permission-mode', 'invalid-mode'])).rejects.toThrow(
/Invalid --permission-mode/
);
await expect(
withScopedHome(() => handlePersistCommand(['glm', '--permission-mode', 'invalid-mode']))
).rejects.toThrow(/Invalid --permission-mode/);
});
it('throws parseError for unknown flags on real handler path', async () => {
await expect(handlePersistCommand(['glm', '--unknown-flag'])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['glm', '--unknown-flag']))).rejects.toThrow(
/Unknown option\(s\)/
);
});
it('throws parseError for list/restore conflict on real handler path', async () => {
await expect(handlePersistCommand(['--list-backups', '--restore'])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['--list-backups', '--restore']))).rejects.toThrow(
'--list-backups cannot be used with --restore'
);
});
it('throws parseError for permission flags with --restore on real handler path', async () => {
await expect(handlePersistCommand(['--restore', '--auto-approve'])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['--restore', '--auto-approve']))).rejects.toThrow(
/Permission flags are not valid with backup operations/
);
});
it('shows help when --help is present even with other invalid args', async () => {
await expect(handlePersistCommand(['--help', '--permission-mode'])).resolves.toBeUndefined();
await expect(withScopedHome(() => handlePersistCommand(['--help', '--permission-mode']))).resolves.toBeUndefined();
});
it('does not create CLAUDE_CONFIG_DIR on parseError path', async () => {
const isolatedClaudeDir = path.join(tempRoot, '.claude-parse-early');
process.env.CLAUDE_CONFIG_DIR = isolatedClaudeDir;
await expect(handlePersistCommand(['glm', '--permission-mode='])).rejects.toThrow(
await expect(withScopedHome(() => handlePersistCommand(['glm', '--permission-mode=']))).rejects.toThrow(
'Missing value for --permission-mode'
);
expect(await pathExists(isolatedClaudeDir)).toBe(false);
@@ -163,9 +159,9 @@ describe('persist command restore failure handling', () => {
stubProcessExit();
try {
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
} finally {
await release();
}
@@ -186,9 +182,9 @@ describe('persist command restore failure handling', () => {
}) as typeof fs.promises.open;
stubProcessExit();
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
});
it('exits when backup read fails with ELOOP (symlink rejection)', async () => {
@@ -206,9 +202,9 @@ describe('persist command restore failure handling', () => {
}) as typeof fs.promises.open;
stubProcessExit();
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
});
it('exits when backup path resolves to a non-regular file', async () => {
@@ -229,9 +225,9 @@ describe('persist command restore failure handling', () => {
}) as typeof fs.promises.open;
stubProcessExit();
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
});
it('rolls back settings when restore write fails mid-flight', async () => {
@@ -248,9 +244,9 @@ describe('persist command restore failure handling', () => {
}) as typeof fs.promises.rename;
stubProcessExit();
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
const finalContent = await fs.promises.readFile(fixture.settingsPath, 'utf8');
const finalSettings = JSON.parse(finalContent);
@@ -273,9 +269,9 @@ describe('persist command restore failure handling', () => {
stubProcessExit();
try {
await expect(handlePersistCommand(['--restore', fixture.timestamp, '--yes'])).rejects.toThrow(
'process.exit(1)'
);
await expect(
withScopedHome(() => handlePersistCommand(['--restore', fixture.timestamp, '--yes']))
).rejects.toThrow('process.exit(1)');
expect(capturedLogs.some((line) => line.includes('Rollback also failed'))).toBe(true);
} finally {
console.log = originalConsoleLog;
@@ -313,7 +309,9 @@ describe('persist command Claude extension parity', () => {
) + '\n',
'utf8'
);
saveUnifiedConfig(config);
await withScopedHome(async () => {
saveUnifiedConfig(config);
});
}
it('persists account profiles via CLAUDE_CONFIG_DIR and clears stale managed env keys', async () => {
@@ -337,7 +335,7 @@ describe('persist command Claude extension parity', () => {
'utf8'
);
await handlePersistCommand(['work', '--yes']);
await withScopedHome(() => handlePersistCommand(['work', '--yes']));
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
env: Record<string, string>;
@@ -371,7 +369,7 @@ describe('persist command Claude extension parity', () => {
'utf8'
);
await handlePersistCommand(['default', '--yes']);
await withScopedHome(() => handlePersistCommand(['default', '--yes']));
const persisted = JSON.parse(await fs.promises.readFile(settingsPath, 'utf8')) as {
env: Record<string, string>;
@@ -4,9 +4,9 @@ import * as os from 'os';
import * as path from 'path';
import { resolveLifecyclePort } from '../../../src/commands/cliproxy/proxy-lifecycle-subcommand';
import { CLIPROXY_DEFAULT_PORT } from '../../../src/cliproxy/config/port-manager';
import { runWithScopedConfigDir } from '../../../src/utils/config-manager';
let tempDir: string;
let originalCcsDir: string | undefined;
function writeUnifiedConfig(localPort: number): void {
const configPath = path.join(tempDir, 'config.yaml');
@@ -34,33 +34,31 @@ cliproxy_server:
describe('resolveLifecyclePort', () => {
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-proxy-lifecycle-'));
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_DIR = tempDir;
});
afterEach(() => {
if (originalCcsDir !== undefined) {
process.env.CCS_DIR = originalCcsDir;
} else {
delete process.env.CCS_DIR;
}
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it('uses configured cliproxy_server.local.port', () => {
it('uses configured cliproxy_server.local.port', async () => {
writeUnifiedConfig(9456);
expect(resolveLifecyclePort()).toBe(9456);
await runWithScopedConfigDir(tempDir, () => {
expect(resolveLifecyclePort()).toBe(9456);
});
});
it('falls back to default port when configured local port is invalid', () => {
it('falls back to default port when configured local port is invalid', async () => {
writeUnifiedConfig(70000);
expect(resolveLifecyclePort()).toBe(CLIPROXY_DEFAULT_PORT);
await runWithScopedConfigDir(tempDir, () => {
expect(resolveLifecyclePort()).toBe(CLIPROXY_DEFAULT_PORT);
});
});
it('falls back to default port when config file is missing', () => {
expect(resolveLifecyclePort()).toBe(CLIPROXY_DEFAULT_PORT);
it('falls back to default port when config file is missing', async () => {
await runWithScopedConfigDir(tempDir, () => {
expect(resolveLifecyclePort()).toBe(CLIPROXY_DEFAULT_PORT);
});
});
});
@@ -60,7 +60,9 @@ describe('web-server claude-extension-routes', () => {
});
afterAll(async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
setGlobalConfigDir(undefined);
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
@@ -1,104 +1,57 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import express from 'express';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { Server } from 'http';
const installSpy = {
calls: 0,
};
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',
latestVersion: '6.6.89',
fromCache: false,
checkedAt: Date.now(),
backend: 'plus',
backendLabel: 'CLIProxy Plus',
isStable: true,
maxStableVersion: '9.9.999-0',
}),
getInstalledCliproxyVersion: () => '6.6.80',
installCliproxyVersion: async () => {},
fetchLatestCliproxyVersion: async () => '6.6.89',
}));
mock.module('../../../src/cliproxy/binary/version-checker', () => ({
fetchAllVersions: async () => ({
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
latestStable: '6.6.89',
latest: '6.6.89',
fromCache: false,
checkedAt: Date.now(),
}),
isNewerVersion: (version: string, maxStable: string) => {
const normalize = (value: string) => value.replace(/-\d+$/, '').split('.').map(Number);
const versionParts = normalize(version);
const maxStableParts = normalize(maxStable);
for (let index = 0; index < 3; index += 1) {
const versionPart = versionParts[index] || 0;
const maxStablePart = maxStableParts[index] || 0;
if (versionPart > maxStablePart) return true;
if (versionPart < maxStablePart) return false;
}
return false;
},
isVersionFaulty: (version: string) =>
['6.6.81', '6.6.82', '6.6.83', '6.6.84', '6.6.85', '6.6.86', '6.6.87', '6.6.88'].includes(
version
),
}));
mock.module('../../../src/web-server/services/cliproxy-dashboard-install-service', () => ({
installDashboardCliproxyVersion: async () => {
installSpy.calls += 1;
return {
success: true,
restarted: true,
port: 8317,
message: 'installed',
};
},
}));
let cliproxyStatsRoutes: typeof import('../../../src/web-server/routes/cliproxy-stats-routes').default;
let createEmptyUnifiedConfig: typeof import('../../../src/config/unified-config-types').createEmptyUnifiedConfig;
let saveUnifiedConfig: typeof import('../../../src/config/unified-config-loader').saveUnifiedConfig;
let setGlobalConfigDir: typeof import('../../../src/utils/config-manager').setGlobalConfigDir;
let writeInstalledVersion: typeof import('../../../src/cliproxy/binary/version-cache').writeInstalledVersion;
let writeVersionListCache: typeof import('../../../src/cliproxy/binary/version-cache').writeVersionListCache;
let server: Server;
let baseUrl = '';
let tempHome = '';
let originalCcsHome: string | undefined;
beforeAll(async () => {
cliproxyStatsRoutes = (await import('../../../src/web-server/routes/cliproxy-stats-routes'))
.default;
originalCcsHome = process.env.CCS_HOME;
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-install-route-'));
process.env.CCS_HOME = tempHome;
({ setGlobalConfigDir } = await import('../../../src/utils/config-manager'));
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
({ writeInstalledVersion, writeVersionListCache } = await import(
'../../../src/cliproxy/binary/version-cache'
));
const ccsDir = path.join(tempHome, '.ccs');
const plusBinDir = path.join(ccsDir, 'cliproxy', 'bin', 'plus');
fs.mkdirSync(plusBinDir, { recursive: true });
setGlobalConfigDir(ccsDir);
const config = createEmptyUnifiedConfig();
config.cliproxy = { backend: 'plus' };
saveUnifiedConfig(config);
writeInstalledVersion(plusBinDir, '6.6.80');
writeVersionListCache(
{
versions: ['6.6.89', '6.6.88', '6.6.81', '6.6.80'],
latestStable: '6.6.89',
latest: '6.6.89',
checkedAt: Date.now(),
},
'plus'
);
({ default: cliproxyStatsRoutes } = await import(
'../../../src/web-server/routes/cliproxy-stats-routes'
));
const app = express();
app.use(express.json());
@@ -118,18 +71,26 @@ beforeAll(async () => {
if (!address || typeof address === 'string') {
throw new Error('Unable to resolve test server port');
}
baseUrl = `http://127.0.0.1:${address.port}`;
});
beforeEach(() => {
installSpy.calls = 0;
baseUrl = `http://127.0.0.1:${address.port}`;
});
afterAll(async () => {
if (server) {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
mock.restore();
setGlobalConfigDir(undefined);
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
if (tempHome && fs.existsSync(tempHome)) {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
describe('cliproxy-stats-routes install contract', () => {
@@ -145,7 +106,7 @@ describe('cliproxy-stats-routes install contract', () => {
expect(body.faultyRange).toEqual({ min: '6.6.81-0', max: '6.6.88-0' });
});
it('returns faulty confirmation metadata without calling the installer', async () => {
it('returns faulty confirmation metadata without attempting the install', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -165,10 +126,9 @@ describe('cliproxy-stats-routes install contract', () => {
expect(body.isFaulty).toBe(true);
expect(body.isExperimental).toBe(false);
expect(body.message).toContain('known bugs');
expect(installSpy.calls).toBe(0);
});
it('returns experimental confirmation metadata without calling the installer', async () => {
it('returns experimental confirmation metadata without attempting the install', async () => {
const response = await fetch(`${baseUrl}/api/cliproxy/install`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -188,6 +148,5 @@ describe('cliproxy-stats-routes install contract', () => {
expect(body.isFaulty).toBe(false);
expect(body.isExperimental).toBe(true);
expect(body.message).toContain('experimental');
expect(installSpy.calls).toBe(0);
});
});
@@ -3,15 +3,12 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { CliproxyUsageApiResponse } from '../../../src/cliproxy/stats-fetcher';
import { runWithScopedConfigDir } from '../../../src/utils/config-manager';
let ccsDir = '';
let rawResponse: CliproxyUsageApiResponse | null = null;
let fetchCalls = 0;
mock.module('../../../src/utils/config-manager', () => ({
getCcsDir: () => ccsDir,
}));
mock.module('../../../src/cliproxy/stats-fetcher', () => ({
fetchCliproxyUsageRaw: async () => {
fetchCalls++;
@@ -69,12 +66,16 @@ afterAll(() => {
describe('cliproxy usage syncer', () => {
it('writes and loads snapshot data', async () => {
await syncer.syncCliproxyUsage();
await runWithScopedConfigDir(ccsDir, async () => {
await syncer.syncCliproxyUsage();
});
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
expect(fs.existsSync(snapshotPath)).toBe(true);
const cached = await syncer.loadCachedCliproxyData();
const cached = await runWithScopedConfigDir(ccsDir, async () => {
return await syncer.loadCachedCliproxyData();
});
expect(cached.daily).toHaveLength(1);
expect(cached.daily[0].source).toBe('cliproxy');
expect(cached.daily[0].inputTokens).toBe(100);
@@ -82,11 +83,13 @@ describe('cliproxy usage syncer', () => {
expect(cached.monthly).toHaveLength(1);
});
it('startCliproxySync is idempotent and starts only one interval', () => {
it('startCliproxySync is idempotent and starts only one interval', async () => {
const intervalSpy = spyOn(globalThis, 'setInterval');
syncer.startCliproxySync();
syncer.startCliproxySync();
await runWithScopedConfigDir(ccsDir, async () => {
syncer.startCliproxySync();
syncer.startCliproxySync();
});
expect(intervalSpy).toHaveBeenCalledTimes(1);
expect(fetchCalls).toBeGreaterThan(0);