mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
refactor: consolidate test structure and remove backup files
- Move ux-integration-test.sh to tests/integration/ directory - Consolidate tests/fixtures/ into tests/shared/fixtures/ - Fix import paths in cli.test.js and postinstall.test.js - Remove src/ccs.ts.backup file (958 lines) - Remove empty src/scripts/ directory - Remove non-existent test:edge-cases script from package.json - Update tests/README.md with accurate structure - Update CONTRIBUTING.md with correct test commands - Update docs/system-architecture.md with correct test paths
This commit is contained in:
1 parent
cb7e38d2b1
commit
72c2b08715
9 files changed
+54
-1041
No files matched your search
+2
-2
@@ -144,8 +144,8 @@ git checkout -b your-feature-name
|
||||
# Test locally with ./ccs
|
||||
|
||||
# Run tests
|
||||
./tests/edge-cases.sh # Unix
|
||||
./tests/edge-cases.ps1 # Windows
|
||||
bun run test # All tests
|
||||
bun run test:native # Native Unix tests only
|
||||
```
|
||||
|
||||
#### Testing npm Package Locally
|
||||
|
||||
@@ -1094,11 +1094,11 @@ graph TD
|
||||
|
||||
```
|
||||
tests/
|
||||
├── shared/
|
||||
│ ├── unit/ # Unit tests for individual modules
|
||||
│ └── fixtures/ # Test data and configurations
|
||||
├── unit/ # Module unit tests (GLMT, delegation)
|
||||
├── shared/ # Shared utilities and fixtures
|
||||
├── npm/ # npm package-specific tests
|
||||
└── edge-cases.sh # Comprehensive scenario testing
|
||||
├── native/ # Native installation tests (bash/PowerShell)
|
||||
└── integration/ # Integration tests
|
||||
```
|
||||
|
||||
### Test Coverage Strategy
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"test:unit": "mocha 'tests/**/unit/**/*.test.js' 'tests/unit/**/*.test.js' --timeout 5000",
|
||||
"test:npm": "mocha tests/npm/**/*.test.js --timeout 10000",
|
||||
"test:native": "bash tests/native/unix/edge-cases.sh",
|
||||
"test:edge-cases": "bash tests/edge-cases.sh",
|
||||
"prepublishOnly": "npm run validate && node scripts/sync-version.js",
|
||||
"prepack": "npm run validate && node scripts/sync-version.js",
|
||||
"prepare": "node scripts/check-executables.js",
|
||||
|
||||
@@ -1,958 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CCS (Claude Code Switch) - Entry Point
|
||||
*
|
||||
* Instant profile switching for Claude CLI.
|
||||
* Supports multiple accounts, alternative models (GLM, Kimi),
|
||||
* and cost-optimized delegation.
|
||||
*/
|
||||
|
||||
import { spawn, ChildProcess, spawnSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { colored } from './utils/helpers';
|
||||
import { detectClaudeCli } from './utils/claude-detector';
|
||||
import { getSettingsPath, getConfigPath } from './utils/config-manager';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { handleHelpCommand } from './commands/help-command';
|
||||
|
||||
// Version (sync with package.json)
|
||||
const CCS_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')
|
||||
).version;
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Escape arguments for shell execution (Windows compatibility)
|
||||
*/
|
||||
function escapeShellArg(arg: string): string {
|
||||
return '"' + String(arg).replace(/"/g, '""') + '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Claude CLI with unified spawn logic
|
||||
*/
|
||||
function execClaude(
|
||||
claudeCli: string,
|
||||
args: string[],
|
||||
envVars: NodeJS.ProcessEnv | null = null
|
||||
): void {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
||||
|
||||
// Prepare environment (merge with process.env if envVars provided)
|
||||
const env = envVars ? { ...process.env, ...envVars } : process.env;
|
||||
|
||||
let child: ChildProcess;
|
||||
if (needsShell) {
|
||||
// When shell needed: concatenate into string to avoid DEP0190 warning
|
||||
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
||||
child = spawn(cmdString, {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
shell: true,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
// When no shell needed: use array form (faster, no shell overhead)
|
||||
child = spawn(claudeCli, args, {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) process.kill(process.pid, signal as NodeJS.Signals);
|
||||
else process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
ErrorManager.showClaudeNotFound();
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Command Handlers ==========
|
||||
|
||||
/**
|
||||
* Handle version command
|
||||
*/
|
||||
function handleVersionCommand(): void {
|
||||
console.log(colored(`CCS (Claude Code Switch) v${CCS_VERSION}`, 'bold'));
|
||||
console.log('');
|
||||
|
||||
console.log(colored('Installation:', 'cyan'));
|
||||
const installLocation = process.argv[1] || '(not found)';
|
||||
console.log(` ${colored('Location:'.padEnd(17), 'cyan')} ${installLocation}`);
|
||||
|
||||
const ccsDir = path.join(os.homedir(), '.ccs');
|
||||
console.log(` ${colored('CCS Directory:'.padEnd(17), 'cyan')} ${ccsDir}`);
|
||||
|
||||
const configPath = getConfigPath();
|
||||
console.log(` ${colored('Config:'.padEnd(17), 'cyan')} ${configPath}`);
|
||||
|
||||
const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json');
|
||||
console.log(` ${colored('Profiles:'.padEnd(17), 'cyan')} ${profilesJson}`);
|
||||
|
||||
// Delegation status
|
||||
const delegationSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
|
||||
const delegationConfigured = fs.existsSync(delegationSessionsPath);
|
||||
|
||||
const readyProfiles: string[] = [];
|
||||
|
||||
// Check for profiles with valid API keys
|
||||
for (const profile of ['glm', 'kimi']) {
|
||||
const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`);
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN;
|
||||
if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) {
|
||||
readyProfiles.push(profile);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasValidApiKeys = readyProfiles.length > 0;
|
||||
const delegationEnabled = delegationConfigured || hasValidApiKeys;
|
||||
|
||||
if (delegationEnabled) {
|
||||
console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Enabled`);
|
||||
} else {
|
||||
console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Not configured`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
if (readyProfiles.length > 0) {
|
||||
console.log(colored('Delegation Ready:', 'cyan'));
|
||||
console.log(
|
||||
` ${colored('[OK]', 'yellow')} ${readyProfiles.join(', ')} profiles are ready for delegation`
|
||||
);
|
||||
console.log('');
|
||||
} else if (delegationEnabled) {
|
||||
console.log(colored('Delegation Ready:', 'cyan'));
|
||||
console.log(` ${colored('[!]', 'yellow')} Delegation configured but no valid API keys found`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`${colored('Documentation:', 'cyan')} https://github.com/kaitranntt/ccs`);
|
||||
console.log(`${colored('License:', 'cyan')} MIT`);
|
||||
console.log('');
|
||||
console.log(colored("Run 'ccs --help' for usage information", 'yellow'));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle help command
|
||||
*/
|
||||
|
||||
function handleInstallCommand(): void {
|
||||
console.log('');
|
||||
console.log('Feature not available');
|
||||
console.log('');
|
||||
console.log('The --install flag is currently under development.');
|
||||
console.log('.claude/ integration testing is not complete.');
|
||||
console.log('');
|
||||
console.log('For updates: https://github.com/kaitranntt/ccs/issues');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function handleUninstallCommand(): void {
|
||||
console.log('');
|
||||
console.log('Feature not available');
|
||||
console.log('');
|
||||
console.log('The --uninstall flag is currently under development.');
|
||||
console.log('.claude/ integration testing is not complete.');
|
||||
console.log('');
|
||||
console.log('For updates: https://github.com/kaitranntt/ccs/issues');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function handleDoctorCommand(): Promise<void> {
|
||||
const DoctorModule = await import('./management/doctor');
|
||||
const Doctor = DoctorModule.default;
|
||||
const doctor = new Doctor();
|
||||
|
||||
await doctor.runAllChecks();
|
||||
|
||||
// Exit with error code if unhealthy
|
||||
process.exit(doctor.isHealthy() ? 0 : 1);
|
||||
}
|
||||
|
||||
async function handleSyncCommand(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(colored('Syncing CCS Components...', 'cyan'));
|
||||
console.log('');
|
||||
|
||||
// First, copy .claude/ directory from package to ~/.ccs/.claude/
|
||||
const { ClaudeDirInstaller } = await import('./utils/claude-dir-installer');
|
||||
const installer = new ClaudeDirInstaller();
|
||||
installer.install();
|
||||
|
||||
console.log('');
|
||||
|
||||
const cleanupResult = installer.cleanupDeprecated();
|
||||
if (cleanupResult.success && cleanupResult.cleanedFiles.length > 0) {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/
|
||||
const { ClaudeSymlinkManager } = await import('./utils/claude-symlink-manager');
|
||||
const manager = new ClaudeSymlinkManager();
|
||||
manager.install(false);
|
||||
|
||||
console.log('');
|
||||
console.log(colored('[OK] Sync complete!', 'green'));
|
||||
console.log('');
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect installation method
|
||||
*/
|
||||
function detectInstallationMethod(): 'npm' | 'direct' {
|
||||
const scriptPath = process.argv[1];
|
||||
|
||||
// Method 1: Check if script is inside node_modules
|
||||
if (scriptPath.includes('node_modules')) {
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
// Method 2: Check if script is in npm global bin directory
|
||||
const npmGlobalBinPatterns = [
|
||||
/\.npm\/global\/bin\//,
|
||||
/\/\.nvm\/versions\/node\/[^/]+\/bin\//,
|
||||
/\/usr\/local\/bin\//,
|
||||
/\/usr\/bin\//,
|
||||
];
|
||||
|
||||
for (const pattern of npmGlobalBinPatterns) {
|
||||
if (pattern.test(scriptPath)) {
|
||||
try {
|
||||
const binDir = path.dirname(scriptPath);
|
||||
const nodeModulesDir = path.join(binDir, '..', 'lib', 'node_modules', '@kaitranntt', 'ccs');
|
||||
const globalModulesDir = path.join(binDir, '..', 'node_modules', '@kaitranntt', 'ccs');
|
||||
|
||||
if (fs.existsSync(nodeModulesDir) || fs.existsSync(globalModulesDir)) {
|
||||
return 'npm';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue checking other patterns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check if package.json exists in parent directory
|
||||
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
||||
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
if (pkg.name === '@kaitranntt/ccs') {
|
||||
return 'npm';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Check if script is a symlink pointing to node_modules
|
||||
try {
|
||||
const stats = fs.lstatSync(scriptPath);
|
||||
if (stats.isSymbolicLink()) {
|
||||
const targetPath = fs.readlinkSync(scriptPath);
|
||||
if (targetPath.includes('node_modules') || targetPath.includes('@kaitranntt/ccs')) {
|
||||
return 'npm';
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to default
|
||||
}
|
||||
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which package manager was used for installation
|
||||
*/
|
||||
function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' {
|
||||
const scriptPath = process.argv[1];
|
||||
|
||||
// Check if script path contains package manager indicators
|
||||
if (scriptPath.includes('.pnpm')) return 'pnpm';
|
||||
if (scriptPath.includes('yarn')) return 'yarn';
|
||||
if (scriptPath.includes('bun')) return 'bun';
|
||||
|
||||
// Check parent directories for lock files
|
||||
const binDir = path.dirname(scriptPath);
|
||||
|
||||
let checkDir = binDir;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (fs.existsSync(path.join(checkDir, 'pnpm-lock.yaml'))) return 'pnpm';
|
||||
if (fs.existsSync(path.join(checkDir, 'yarn.lock'))) return 'yarn';
|
||||
if (fs.existsSync(path.join(checkDir, 'bun.lockb'))) return 'bun';
|
||||
checkDir = path.dirname(checkDir);
|
||||
}
|
||||
|
||||
// Check if package managers are available on the system
|
||||
try {
|
||||
const yarnResult = spawnSync('yarn', ['global', 'list', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (yarnResult.status === 0 && yarnResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'yarn';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to next check
|
||||
}
|
||||
|
||||
try {
|
||||
const pnpmResult = spawnSync('pnpm', ['list', '-g', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (pnpmResult.status === 0 && pnpmResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'pnpm';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to next check
|
||||
}
|
||||
|
||||
try {
|
||||
const bunResult = spawnSync('bun', ['pm', 'ls', '-g', '--pattern', '@kaitranntt/ccs'], {
|
||||
encoding: 'utf8',
|
||||
shell: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (bunResult.status === 0 && bunResult.stdout.includes('@kaitranntt/ccs')) {
|
||||
return 'bun';
|
||||
}
|
||||
} catch (_err) {
|
||||
// Continue to default
|
||||
}
|
||||
|
||||
return 'npm';
|
||||
}
|
||||
|
||||
async function handleUpdateCommand(): Promise<void> {
|
||||
const { checkForUpdates } = await import('./utils/update-checker');
|
||||
|
||||
console.log('');
|
||||
console.log(colored('Checking for updates...', 'cyan'));
|
||||
console.log('');
|
||||
|
||||
const installMethod = detectInstallationMethod();
|
||||
const isNpmInstall = installMethod === 'npm';
|
||||
|
||||
const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod);
|
||||
|
||||
if (updateResult.status === 'check_failed') {
|
||||
console.log(colored(`[X] ${updateResult.message}`, 'red'));
|
||||
console.log('');
|
||||
console.log(colored('[i] Possible causes:', 'yellow'));
|
||||
console.log(' - Network connection issues');
|
||||
console.log(' - Firewall blocking requests');
|
||||
console.log(' - GitHub/npm API temporarily unavailable');
|
||||
console.log('');
|
||||
console.log('Try again later or update manually:');
|
||||
if (isNpmInstall) {
|
||||
const packageManager = detectPackageManager();
|
||||
let manualCommand: string;
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
||||
break;
|
||||
case 'yarn':
|
||||
manualCommand = 'yarn global add @kaitranntt/ccs@latest';
|
||||
break;
|
||||
case 'pnpm':
|
||||
manualCommand = 'pnpm add -g @kaitranntt/ccs@latest';
|
||||
break;
|
||||
case 'bun':
|
||||
manualCommand = 'bun add -g @kaitranntt/ccs@latest';
|
||||
break;
|
||||
default:
|
||||
manualCommand = 'npm install -g @kaitranntt/ccs@latest';
|
||||
}
|
||||
|
||||
console.log(colored(` ${manualCommand}`, 'yellow'));
|
||||
} else {
|
||||
const isWindows = process.platform === 'win32';
|
||||
if (isWindows) {
|
||||
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
||||
} else {
|
||||
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (updateResult.status === 'no_update') {
|
||||
let message = `You are already on the latest version (${CCS_VERSION})`;
|
||||
|
||||
switch (updateResult.reason) {
|
||||
case 'dismissed':
|
||||
message = `Update dismissed. You are on version ${CCS_VERSION}`;
|
||||
console.log(colored(`[i] ${message}`, 'yellow'));
|
||||
break;
|
||||
case 'cached':
|
||||
message = `No updates available (cached result). You are on version ${CCS_VERSION}`;
|
||||
console.log(colored(`[i] ${message}`, 'cyan'));
|
||||
break;
|
||||
default:
|
||||
console.log(colored(`[OK] ${message}`, 'green'));
|
||||
}
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Update available
|
||||
console.log(
|
||||
colored(`[i] Update available: ${updateResult.current} -> ${updateResult.latest}`, 'yellow')
|
||||
);
|
||||
console.log('');
|
||||
|
||||
if (isNpmInstall) {
|
||||
const packageManager = detectPackageManager();
|
||||
let updateCommand: string;
|
||||
let updateArgs: string[];
|
||||
let cacheCommand: string | null;
|
||||
let cacheArgs: string[] | null;
|
||||
|
||||
switch (packageManager) {
|
||||
case 'npm':
|
||||
updateCommand = 'npm';
|
||||
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
||||
cacheCommand = 'npm';
|
||||
cacheArgs = ['cache', 'clean', '--force'];
|
||||
break;
|
||||
case 'yarn':
|
||||
updateCommand = 'yarn';
|
||||
updateArgs = ['global', 'add', '@kaitranntt/ccs@latest'];
|
||||
cacheCommand = 'yarn';
|
||||
cacheArgs = ['cache', 'clean'];
|
||||
break;
|
||||
case 'pnpm':
|
||||
updateCommand = 'pnpm';
|
||||
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
||||
cacheCommand = 'pnpm';
|
||||
cacheArgs = ['store', 'prune'];
|
||||
break;
|
||||
case 'bun':
|
||||
updateCommand = 'bun';
|
||||
updateArgs = ['add', '-g', '@kaitranntt/ccs@latest'];
|
||||
cacheCommand = null;
|
||||
cacheArgs = null;
|
||||
break;
|
||||
default:
|
||||
updateCommand = 'npm';
|
||||
updateArgs = ['install', '-g', '@kaitranntt/ccs@latest'];
|
||||
cacheCommand = 'npm';
|
||||
cacheArgs = ['cache', 'clean', '--force'];
|
||||
}
|
||||
|
||||
console.log(colored(`Updating via ${packageManager}...`, 'cyan'));
|
||||
console.log('');
|
||||
|
||||
const performUpdate = (): void => {
|
||||
const child = spawn(updateCommand, updateArgs, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('');
|
||||
console.log(colored('[OK] Update successful!', 'green'));
|
||||
console.log('');
|
||||
console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log(colored('[X] Update failed', 'red'));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
||||
console.log('');
|
||||
}
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
console.log('');
|
||||
console.log(colored(`[X] Failed to run ${packageManager} update`, 'red'));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow'));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
if (cacheCommand && cacheArgs) {
|
||||
console.log(colored('Clearing package cache...', 'cyan'));
|
||||
const cacheChild = spawn(cacheCommand, cacheArgs, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
cacheChild.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
|
||||
}
|
||||
performUpdate();
|
||||
});
|
||||
|
||||
cacheChild.on('error', () => {
|
||||
console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow'));
|
||||
performUpdate();
|
||||
});
|
||||
} else {
|
||||
performUpdate();
|
||||
}
|
||||
} else {
|
||||
// Direct installation - re-run installer
|
||||
console.log(colored('Updating via installer...', 'cyan'));
|
||||
console.log('');
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
let command: string;
|
||||
let args: string[];
|
||||
|
||||
if (isWindows) {
|
||||
command = 'powershell.exe';
|
||||
args = [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-Command',
|
||||
'irm ccs.kaitran.ca/install | iex',
|
||||
];
|
||||
} else {
|
||||
command = '/bin/bash';
|
||||
args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash'];
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('');
|
||||
console.log(colored('[OK] Update successful!', 'green'));
|
||||
console.log('');
|
||||
console.log(`Run ${colored('ccs --version', 'yellow')} to verify`);
|
||||
console.log('');
|
||||
} else {
|
||||
console.log('');
|
||||
console.log(colored('[X] Update failed', 'red'));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
if (isWindows) {
|
||||
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
||||
} else {
|
||||
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
console.log('');
|
||||
console.log(colored('[X] Failed to run installer', 'red'));
|
||||
console.log('');
|
||||
console.log('Try manually:');
|
||||
if (isWindows) {
|
||||
console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow'));
|
||||
} else {
|
||||
console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow'));
|
||||
}
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Profile Detection ==========
|
||||
|
||||
interface DetectedProfile {
|
||||
profile: string;
|
||||
remainingArgs: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart profile detection
|
||||
*/
|
||||
function detectProfile(args: string[]): DetectedProfile {
|
||||
if (args.length === 0 || args[0].startsWith('-')) {
|
||||
// No args or first arg is a flag → use default profile
|
||||
return { profile: 'default', remainingArgs: args };
|
||||
} else {
|
||||
// First arg doesn't start with '-' → treat as profile name
|
||||
return { profile: args[0], remainingArgs: args.slice(1) };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GLMT Proxy Execution ==========
|
||||
|
||||
/**
|
||||
* Execute Claude CLI with embedded proxy (for GLMT profile)
|
||||
*/
|
||||
async function execClaudeWithProxy(
|
||||
claudeCli: string,
|
||||
profileName: string,
|
||||
args: string[]
|
||||
): Promise<void> {
|
||||
// 1. Read settings to get API key
|
||||
const settingsPath = getSettingsPath(profileName);
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN;
|
||||
|
||||
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
||||
console.error('[X] GLMT profile requires Z.AI API key');
|
||||
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Detect verbose flag
|
||||
const verbose = args.includes('--verbose') || args.includes('-v');
|
||||
|
||||
// 2. Spawn embedded proxy with verbose flag
|
||||
const proxyPath = path.join(__dirname, 'glmt', 'glmt-proxy.js');
|
||||
const proxyArgs = verbose ? ['--verbose'] : [];
|
||||
// Use process.execPath for Windows compatibility (CVE-2024-27980)
|
||||
const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], {
|
||||
stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit'],
|
||||
});
|
||||
|
||||
// 3. Wait for proxy ready signal (with timeout)
|
||||
const { ProgressIndicator } = await import('./utils/progress-indicator');
|
||||
const spinner = new ProgressIndicator('Starting GLMT proxy');
|
||||
spinner.start();
|
||||
|
||||
let port: number;
|
||||
try {
|
||||
port = await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Proxy startup timeout (5s)'));
|
||||
}, 5000);
|
||||
|
||||
proxy.stdout?.on('data', (data: Buffer) => {
|
||||
const match = data.toString().match(/PROXY_READY:(\d+)/);
|
||||
if (match) {
|
||||
clearTimeout(timeout);
|
||||
resolve(parseInt(match[1]));
|
||||
}
|
||||
});
|
||||
|
||||
proxy.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
proxy.on('exit', (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Proxy exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
spinner.succeed(`GLMT proxy ready on port ${port}`);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
spinner.fail('Failed to start GLMT proxy');
|
||||
console.error('[X] Error:', err.message);
|
||||
console.error('');
|
||||
console.error('Possible causes:');
|
||||
console.error(' 1. Port conflict (unlikely with random port)');
|
||||
console.error(' 2. Node.js permission issue');
|
||||
console.error(' 3. Firewall blocking localhost');
|
||||
console.error('');
|
||||
console.error('Workarounds:');
|
||||
console.error(' - Use non-thinking mode: ccs glm "prompt"');
|
||||
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
|
||||
console.error(' - Check proxy logs in ~/.ccs/logs/ (if debug enabled)');
|
||||
console.error('');
|
||||
proxy.kill();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. Spawn Claude CLI with proxy URL
|
||||
const envVars: NodeJS.ProcessEnv = {
|
||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: 'glm-4.6',
|
||||
};
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
||||
const env = { ...process.env, ...envVars };
|
||||
|
||||
let claude: ChildProcess;
|
||||
if (needsShell) {
|
||||
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
||||
claude = spawn(cmdString, {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
shell: true,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
claude = spawn(claudeCli, args, {
|
||||
stdio: 'inherit',
|
||||
windowsHide: true,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Cleanup: kill proxy when Claude exits
|
||||
claude.on('exit', (code, signal) => {
|
||||
proxy.kill('SIGTERM');
|
||||
if (signal) process.kill(process.pid, signal as NodeJS.Signals);
|
||||
else process.exit(code || 0);
|
||||
});
|
||||
|
||||
claude.on('error', (error) => {
|
||||
console.error('[X] Claude CLI error:', error);
|
||||
proxy.kill('SIGTERM');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Also handle parent process termination
|
||||
process.once('SIGTERM', () => {
|
||||
proxy.kill('SIGTERM');
|
||||
claude.kill('SIGTERM');
|
||||
});
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
proxy.kill('SIGTERM');
|
||||
claude.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle shell completion installation
|
||||
*/
|
||||
async function handleShellCompletionCommand(args: string[]): Promise<void> {
|
||||
const { ShellCompletionInstaller } = await import('./utils/shell-completion');
|
||||
|
||||
console.log(colored('Shell Completion Installer', 'bold'));
|
||||
console.log('');
|
||||
|
||||
// Parse flags
|
||||
let targetShell: string | null = null;
|
||||
if (args.includes('--bash')) targetShell = 'bash';
|
||||
else if (args.includes('--zsh')) targetShell = 'zsh';
|
||||
else if (args.includes('--fish')) targetShell = 'fish';
|
||||
else if (args.includes('--powershell')) targetShell = 'powershell';
|
||||
|
||||
try {
|
||||
const installer = new ShellCompletionInstaller();
|
||||
const result = installer.install(targetShell as 'bash' | 'zsh' | 'fish' | 'powershell' | null);
|
||||
|
||||
if (result.alreadyInstalled) {
|
||||
console.log(colored('[OK] Shell completion already installed', 'green'));
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(colored('[OK] Shell completion installed successfully!', 'green'));
|
||||
console.log('');
|
||||
console.log(result.message);
|
||||
console.log('');
|
||||
console.log(colored('To activate:', 'cyan'));
|
||||
console.log(` ${result.reload}`);
|
||||
console.log('');
|
||||
console.log(colored('Then test:', 'cyan'));
|
||||
console.log(' ccs <TAB> # See available profiles');
|
||||
console.log(' ccs auth <TAB> # See auth subcommands');
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error(colored('[X] Error:', 'red'), err.message);
|
||||
console.error('');
|
||||
console.error(colored('Usage:', 'yellow'));
|
||||
console.error(' ccs --shell-completion # Auto-detect shell');
|
||||
console.error(' ccs --shell-completion --bash # Install for bash');
|
||||
console.error(' ccs --shell-completion --zsh # Install for zsh');
|
||||
console.error(' ccs --shell-completion --fish # Install for fish');
|
||||
console.error(' ccs --shell-completion --powershell # Install for PowerShell');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main Execution ==========
|
||||
|
||||
interface ProfileError extends Error {
|
||||
profileName?: string;
|
||||
availableProfiles?: string;
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Special case: version command (check BEFORE profile detection)
|
||||
const firstArg = args[0];
|
||||
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
||||
handleVersionCommand();
|
||||
}
|
||||
|
||||
// Special case: help command
|
||||
if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') {
|
||||
handleHelpCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: install command
|
||||
if (firstArg === '--install') {
|
||||
handleInstallCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: uninstall command
|
||||
if (firstArg === '--uninstall') {
|
||||
handleUninstallCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: shell completion installer
|
||||
if (firstArg === '--shell-completion' || firstArg === '-sc') {
|
||||
await handleShellCompletionCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: doctor command
|
||||
if (firstArg === 'doctor' || firstArg === '--doctor') {
|
||||
await handleDoctorCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: sync command
|
||||
if (firstArg === 'sync' || firstArg === '--sync') {
|
||||
await handleSyncCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: update command
|
||||
if (firstArg === 'update' || firstArg === '--update') {
|
||||
await handleUpdateCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: auth command
|
||||
if (firstArg === 'auth') {
|
||||
const AuthCommandsModule = await import('./auth/auth-commands');
|
||||
const AuthCommands = AuthCommandsModule.default;
|
||||
const authCommands = new AuthCommands();
|
||||
await authCommands.route(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: headless delegation (-p flag)
|
||||
if (args.includes('-p') || args.includes('--prompt')) {
|
||||
const { DelegationHandler } = await import('./delegation/delegation-handler');
|
||||
const handler = new DelegationHandler();
|
||||
await handler.route(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-recovery for missing configuration
|
||||
const RecoveryManagerModule = await import('./management/recovery-manager');
|
||||
const RecoveryManager = RecoveryManagerModule.default;
|
||||
const recovery = new RecoveryManager();
|
||||
const recovered = recovery.recoverAll();
|
||||
|
||||
if (recovered) {
|
||||
recovery.showRecoveryHints();
|
||||
}
|
||||
|
||||
// Detect profile
|
||||
const { profile, remainingArgs } = detectProfile(args);
|
||||
|
||||
// Detect Claude CLI first (needed for all paths)
|
||||
const claudeCli = detectClaudeCli();
|
||||
if (!claudeCli) {
|
||||
ErrorManager.showClaudeNotFound();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Use ProfileDetector to determine profile type
|
||||
const ProfileDetectorModule = await import('./auth/profile-detector');
|
||||
const ProfileDetector = ProfileDetectorModule.default;
|
||||
const InstanceManagerModule = await import('./management/instance-manager');
|
||||
const InstanceManager = InstanceManagerModule.default;
|
||||
const ProfileRegistryModule = await import('./auth/profile-registry');
|
||||
const ProfileRegistry = ProfileRegistryModule.default;
|
||||
|
||||
const detector = new ProfileDetector();
|
||||
|
||||
try {
|
||||
const profileInfo = detector.detectProfileType(profile);
|
||||
|
||||
if (profileInfo.type === 'settings') {
|
||||
// Check if this is GLMT profile (requires proxy)
|
||||
if (profileInfo.name === 'glmt') {
|
||||
// GLMT FLOW: Settings-based with embedded proxy for thinking support
|
||||
await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs);
|
||||
} else {
|
||||
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
||||
// Use --settings flag (backward compatible)
|
||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
||||
}
|
||||
} else if (profileInfo.type === 'account') {
|
||||
// NEW FLOW: Account-based profile (work, personal)
|
||||
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
||||
const registry = new ProfileRegistry();
|
||||
const instanceMgr = new InstanceManager();
|
||||
|
||||
// Ensure instance exists (lazy init if needed)
|
||||
const instancePath = instanceMgr.ensureInstance(profileInfo.name);
|
||||
|
||||
// Update last_used timestamp
|
||||
registry.touchProfile(profileInfo.name);
|
||||
|
||||
// Execute Claude with instance isolation
|
||||
const envVars: NodeJS.ProcessEnv = { CLAUDE_CONFIG_DIR: instancePath };
|
||||
execClaude(claudeCli, remainingArgs, envVars);
|
||||
} else {
|
||||
// DEFAULT: No profile configured, use Claude's own defaults
|
||||
execClaude(claudeCli, remainingArgs);
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as ProfileError;
|
||||
// Check if this is a profile not found error with suggestions
|
||||
if (err.profileName && err.availableProfiles !== undefined) {
|
||||
const allProfiles = err.availableProfiles.split('\n');
|
||||
ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions);
|
||||
} else {
|
||||
console.error(`[X] ${err.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run main
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
+46
-74
@@ -2,90 +2,62 @@
|
||||
|
||||
## Organization
|
||||
|
||||
- `native/` - Native installation tests only (curl|bash, irm|iex)
|
||||
- `unix/` - Unix/Linux/macOS native tests (37 tests)
|
||||
- `windows/` - Windows PowerShell tests
|
||||
- `npm/` - npm package tests only (39 tests)
|
||||
- `postinstall.test.js` - Postinstall behavior tests (Section 10)
|
||||
- `cli.test.js` - CLI argument parsing tests
|
||||
- `cross-platform.test.js` - Cross-platform compatibility tests
|
||||
- `special-commands.test.js` - npm package integration tests
|
||||
- `shared/` - Shared utilities, fixtures, and unit tests
|
||||
- `helpers.sh` - Bash test utilities and functions
|
||||
- `test-data.js` - Test data for npm tests
|
||||
- `fixtures/` - Test configuration files
|
||||
- `unit/` - Unit tests for helper functions
|
||||
- `unit/` - Module unit tests (GLMT, delegation)
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Module unit tests (Mocha)
|
||||
│ ├── glmt/ # GLMT transformer tests
|
||||
│ └── delegation/ # Delegation module tests
|
||||
├── npm/ # npm package tests (Mocha)
|
||||
├── native/ # Native installation tests (bash/PowerShell)
|
||||
│ ├── unix/ # Unix/Linux/macOS tests
|
||||
│ └── windows/ # Windows PowerShell tests
|
||||
├── integration/ # Integration tests (manual execution)
|
||||
└── shared/ # Shared utilities
|
||||
├── fixtures/ # Test configuration and environment
|
||||
├── unit/ # Helper function tests
|
||||
├── helpers.sh # Bash test utilities
|
||||
└── test-data.js # Test data for npm tests
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
bun run test # All tests (177 total)
|
||||
bun run test:unit # Unit tests only (138)
|
||||
bun run test:npm # npm package tests (39)
|
||||
bun run test:native # Native installation tests (37)
|
||||
bun run test:edge-cases # Master orchestrator (backward compatible)
|
||||
bun run test # All automated tests (unit + npm)
|
||||
bun run test:unit # Unit tests only (Mocha)
|
||||
bun run test:npm # npm package tests (Mocha)
|
||||
bun run test:native # Native Unix tests (bash)
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
## Test Categories
|
||||
|
||||
### Native Tests (`native/`)
|
||||
Test the traditional installation methods where CCS is installed via:
|
||||
- Unix/Linux/macOS: `curl | bash` → tests `lib/ccs`
|
||||
- Windows: `irm | iex` (PowerShell) → tests `lib/ccs.ps1`
|
||||
|
||||
These tests use bash/PowerShell scripts and cover Sections 1-9 from the original edge-cases.sh.
|
||||
|
||||
**Files**:
|
||||
- `native/unix/edge-cases.sh` - 37 native Unix tests
|
||||
- `native/windows/edge-cases.ps1` - Windows PowerShell tests
|
||||
- `native/unix/install.sh` - Unix installation tests
|
||||
- `native/windows/install.ps1` - Windows installation tests
|
||||
### Unit Tests (`unit/`)
|
||||
Module-level tests using Mocha framework:
|
||||
- `unit/glmt/` - GLMT transformer, SSE parser, delta accumulator
|
||||
- `unit/delegation/` - Permission mode, session manager, result formatter
|
||||
|
||||
### npm Tests (`npm/`)
|
||||
Test the npm package installation where CCS is installed via:
|
||||
- npm: `npm install -g @kaitranntt/ccs` → tests `bin/ccs.js`
|
||||
npm package functionality tests using Mocha:
|
||||
- `postinstall.test.js` - Postinstall behavior
|
||||
- `cli.test.js` - CLI argument parsing
|
||||
- `cross-platform.test.js` - Cross-platform compatibility
|
||||
- `special-commands.test.js` - Integration tests
|
||||
|
||||
These tests use Node.js/mocha framework and include Section 10 (postinstall) plus CLI tests.
|
||||
### Native Tests (`native/`)
|
||||
Installation tests for curl|bash (Unix) and irm|iex (Windows):
|
||||
- `native/unix/edge-cases.sh` - Unix edge case tests
|
||||
- `native/windows/edge-cases.ps1` - Windows edge case tests
|
||||
|
||||
**Files**:
|
||||
- `npm/postinstall.test.js` - 6 postinstall behavior tests (Section 10)
|
||||
- `npm/cli.test.js` - 15 CLI argument parsing tests
|
||||
- `npm/cross-platform.test.js` - 13 cross-platform compatibility tests
|
||||
- `npm/special-commands.test.js` - 5 integration tests for npm package
|
||||
### Integration Tests (`integration/`)
|
||||
Manual execution tests for specific scenarios:
|
||||
- `token-counting-test.js` - Token counting validation
|
||||
- `z-ai-streaming-test.js` - Z.AI streaming
|
||||
- `glmt-integration-test.sh` - GLMT integration
|
||||
- `symlink-chain-test.sh` - Symlink chain handling
|
||||
- `ux-integration-test.sh` - CLI UX integration
|
||||
|
||||
### Shared Resources (`shared/`)
|
||||
Common test code, data, and helper functions shared across test suites to avoid duplication.
|
||||
## Adding New Tests
|
||||
|
||||
**Files**:
|
||||
- `shared/helpers.sh` - Bash test utilities and functions
|
||||
- `shared/test-data.js` - Test data for npm tests
|
||||
- `shared/fixtures/` - Test configuration files
|
||||
- `shared/unit/` - Unit tests for helper functions
|
||||
- `unit/glmt/` - GLMT transformer unit tests
|
||||
- `unit/delegation/` - Delegation module unit tests
|
||||
|
||||
## Test Counts
|
||||
|
||||
| Test Type | Count | Location |
|
||||
|-----------|-------|----------|
|
||||
| Native Unix | 37 | `native/unix/` |
|
||||
| npm Package | 39 | `npm/` |
|
||||
| Unit Tests | 138 | `shared/unit/`, `unit/glmt/`, `unit/delegation/` |
|
||||
| **Total** | **177** | **All suites** |
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
All existing commands still work:
|
||||
- `bash tests/edge-cases.sh` - Master orchestrator (runs all tests)
|
||||
- `npm test` - Now runs comprehensive test suite
|
||||
- No breaking changes to existing workflows
|
||||
|
||||
## Migration Notes
|
||||
|
||||
This restructure solves the original problem where Section 10 (npm postinstall tests) was buried in the native `edge-cases.sh` file. Now:
|
||||
- ✅ Clear separation: npm tests in `npm/`, native in `native/`
|
||||
- ✅ Targeted execution: `npm run test:npm` vs `npm run test:native`
|
||||
- ✅ Better organization: Obvious where to add new tests
|
||||
- ✅ DRY principle: Shared utilities in `shared/`
|
||||
- ✅ Increased coverage: From 41 to 177 tests
|
||||
- **Unit tests**: Add to `unit/<module>/` using Mocha + Node.js assert
|
||||
- **npm tests**: Add to `npm/` using Mocha
|
||||
- **Native tests**: Add to `native/unix/` or `native/windows/`
|
||||
- **Integration tests**: Add to `integration/`
|
||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const { createTestEnvironment } = require('../fixtures/test-environment');
|
||||
const { createTestEnvironment } = require('../shared/fixtures/test-environment');
|
||||
|
||||
describe('npm CLI', () => {
|
||||
const ccsPath = path.join(__dirname, '..', '..', 'dist', 'ccs.js');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const { createTestEnvironment } = require('../fixtures/test-environment');
|
||||
const { createTestEnvironment } = require('../shared/fixtures/test-environment');
|
||||
|
||||
describe('npm postinstall', () => {
|
||||
let testEnv;
|
||||
|
||||
File renamed without changes.
Reference in new issue
Block a user