UNPKG

consortium

Version:

Remote control and session sharing CLI for AI coding agents

373 lines (306 loc) 15 kB
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import { findGlobalClaudeCliPath, findClaudeInPath, detectSourceFromPath, findNpmGlobalCliPath, findBunGlobalCliPath, findHomebrewCliPath, findNativeInstallerCliPath, getVersion, compareVersions } from '../scripts/claude_version_utils.cjs'; describe('Claude Version Utils - Cross-Platform Detection', () => { describe('detectSourceFromPath', () => { describe('npm installations', () => { it('should detect npm global installation on macOS/Linux', () => { const result = detectSourceFromPath('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js'); expect(result).toBe('npm'); }); it('should detect npm global installation on Windows with forward slashes', () => { const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js'); expect(result).toBe('npm'); }); it('should detect npm global installation on Windows with backslashes', () => { const result = detectSourceFromPath('C:\\Users\\test\\AppData\\Roaming\\npm\\node_modules\\@anthropic-ai\\claude-code\\cli.js'); expect(result).toBe('npm'); }); it('should detect npm with different scoped packages', () => { const result = detectSourceFromPath('C:/Users/test/AppData/Roaming/npm/node_modules/@babel/core/cli.js'); expect(result).toBe('npm'); }); it('should detect npm through Homebrew', () => { const result = detectSourceFromPath('/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js'); expect(result).toBe('npm'); }); it('should NOT detect Homebrew cask as npm', () => { const result = detectSourceFromPath('/opt/homebrew/lib/node_modules/@anthropic-ai/.claude-code-2DTsDk1V/cli.js'); expect(result).toBe('Homebrew'); }); }); describe('Bun installations', () => { it('should detect Bun global installation on Unix', () => { const result = detectSourceFromPath('/Users/test/.bun/bin/claude'); expect(result).toBe('Bun'); }); it('should detect Bun global installation on Windows', () => { const result = detectSourceFromPath('C:/Users/test/.bun/bin/claude'); expect(result).toBe('Bun'); }); it('should detect Bun with @ symbol in username', () => { const result = detectSourceFromPath('C:/Users/@specialuser/.bun/bin/claude'); expect(result).toBe('Bun'); }); it('should detect Bun in node_modules context', () => { const result = detectSourceFromPath('/Users/test/.bun/install/global/node_modules/@anthropic-ai/claude-code/cli.js'); expect(result).toBe('Bun'); }); }); describe('Homebrew installations', () => { it('should detect Homebrew on Apple Silicon macOS', () => { const result = detectSourceFromPath('/opt/homebrew/bin/claude'); expect(result).toBe('Homebrew'); }); it('should detect Homebrew on Intel macOS', () => { // Mock macOS platform const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }); const result = detectSourceFromPath('/usr/local/bin/claude'); expect(result).toBe('Homebrew'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); }); it('should detect native installer on Linux for /usr/local/bin/claude', () => { // Mock Linux platform const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }); const result = detectSourceFromPath('/usr/local/bin/claude'); expect(result).toBe('native installer'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); }); it('should detect Homebrew on Linux', () => { const result = detectSourceFromPath('/home/linuxbrew/.linuxbrew/bin/claude'); expect(result).toBe('Homebrew'); }); it('should detect Homebrew user installation', () => { const result = detectSourceFromPath('/Users/test/.linuxbrew/bin/claude'); expect(result).toBe('Homebrew'); }); it('should detect Homebrew cask with hashed directory', () => { const result = detectSourceFromPath('/opt/homebrew/lib/node_modules/@anthropic-ai/.claude-code-2DTsDk1V/cli.js'); expect(result).toBe('Homebrew'); }); it('should detect Homebrew Cellar installation', () => { const result = detectSourceFromPath('/opt/homebrew/Cellar/claude-code/1.0.0/bin/claude'); expect(result).toBe('Homebrew'); }); }); describe('Native installer installations', () => { it('should detect native installer on Unix ~/.local', () => { const result = detectSourceFromPath('/Users/test/.local/bin/claude'); expect(result).toBe('native installer'); }); it('should detect native installer with versioned structure', () => { const result = detectSourceFromPath('/Users/test/.local/share/claude/versions/2.0.69/claude'); expect(result).toBe('native installer'); }); it('should detect native installer on Windows Program Files', () => { const result = detectSourceFromPath('C:/Program Files/Claude/claude.exe'); expect(result).toBe('native installer'); }); it('should detect native installer on Windows AppData', () => { const result = detectSourceFromPath('C:/Users/test/AppData/Local/Claude/claude.exe'); expect(result).toBe('native installer'); }); it('should detect native installer on Windows custom location', () => { const result = detectSourceFromPath('E:/Tools/Claude/claude.exe'); expect(result).toBe('native installer'); }); it('should detect native installer on Windows D: drive', () => { const result = detectSourceFromPath('D:/Development/Claude/claude.exe'); expect(result).toBe('native installer'); }); it('should detect native installer in user profile', () => { const result = detectSourceFromPath('C:/Users/test/.claude/claude.exe'); expect(result).toBe('native installer'); }); }); describe('Edge cases and special characters', () => { it('should handle @ symbols in paths correctly', () => { const result = detectSourceFromPath('/Users/@developer/test/node_modules/@anthropic-ai/claude-code/cli.js'); expect(result).toBe('npm'); }); it('should handle case sensitivity variations on Windows', () => { const result = detectSourceFromPath('C:/USERS/TEST/APPDATA/ROAMING/NPM/NODE_MODULES/@ANTHROPIC-AI/CLAUDE-CODE/CLI.JS'); expect(result).toBe('npm'); }); it('should return PATH for unrecognized paths', () => { const result = detectSourceFromPath('/some/random/path/claude'); expect(result).toBe('PATH'); }); it('should handle empty paths', () => { const result = detectSourceFromPath(''); expect(result).toBe('PATH'); }); it('should handle relative paths', () => { const result = detectSourceFromPath('./local/bin/claude'); expect(result).toBe('PATH'); }); }); }); describe('Cross-platform compatibility', () => { it('should handle both forward and backward slashes', () => { const forward = detectSourceFromPath('C:/Users/test/AppData/Local/Claude/claude.exe'); const backward = detectSourceFromPath('C:\\Users\\test\\AppData\\Local\\Claude\\claude.exe'); expect(forward).toBe('native installer'); expect(backward).toBe('native installer'); }); it('should handle Windows drive letters', () => { const drives = ['C:', 'D:', 'E:', 'Z:']; drives.forEach(drive => { const result = detectSourceFromPath(`${drive}/Program Files/Claude/claude.exe`); expect(result).toBe('native installer'); }); }); it('should handle Unix-style absolute paths', () => { const unixPaths = [ '/usr/local/bin/claude', '/opt/homebrew/bin/claude', '/home/user/.local/bin/claude' ]; unixPaths.forEach(path => { const result = detectSourceFromPath(path); expect(['Homebrew', 'native installer']).toContain(result); }); }); }); describe('Version comparison', () => { it('should compare versions correctly', () => { expect(compareVersions('2.0.69', '2.0.68')).toBe(1); expect(compareVersions('2.0.68', '2.0.69')).toBe(-1); expect(compareVersions('2.0.69', '2.0.69')).toBe(0); expect(compareVersions('2.1.0', '2.0.69')).toBe(1); expect(compareVersions('1.0.0', '2.0.0')).toBe(-1); }); it('should handle malformed versions gracefully', () => { expect(() => compareVersions('', '2.0.0')).not.toThrow(); expect(() => compareVersions('invalid', '2.0.0')).not.toThrow(); expect(() => compareVersions('2.0.0', '')).not.toThrow(); }); }); describe('Integration scenarios', () => { it('should handle multiple installations scenario', () => { const scenarios = [ { path: '/Users/test/.bun/bin/claude', expected: 'Bun' }, { path: '/opt/homebrew/bin/claude', expected: 'Homebrew' }, { path: '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js', expected: 'npm' }, { path: 'C:/Program Files/Claude/claude.exe', expected: 'native installer' } ]; scenarios.forEach(({ path, expected }) => { const result = detectSourceFromPath(path); expect(result).toBe(expected); }); }); it('should maintain 100% success rate on all standard installation patterns', () => { const standardPatterns = [ // npm (most common) { path: '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js', expected: 'npm' }, { path: 'C:/Users/test/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code/cli.js', expected: 'npm' }, // bun (second most common) { path: '/Users/test/.bun/bin/claude', expected: 'Bun' }, { path: 'C:/Users/test/.bun/bin/claude', expected: 'Bun' }, // homebrew (macOS and Linux) { path: '/opt/homebrew/bin/claude', expected: 'Homebrew' }, { path: '/home/linuxbrew/.linuxbrew/bin/claude', expected: 'Homebrew' }, { path: '/Users/test/.linuxbrew/bin/claude', expected: 'Homebrew' }, // LinuxBrew user installation // native installers { path: 'C:/Program Files/Claude/claude.exe', expected: 'native installer' }, { path: 'C:/Users/test/AppData/Local/Claude/claude.exe', expected: 'native installer' }, { path: '/Users/test/.local/bin/claude', expected: 'native installer' } ]; let passed = 0; standardPatterns.forEach(({ path, expected }) => { const result = detectSourceFromPath(path); if (result === expected) passed++; }); expect(passed).toBe(standardPatterns.length); expect(passed / standardPatterns.length).toBe(1); // 100% success rate }); it('should handle platform-specific /usr/local/bin/claude correctly', () => { const originalPlatform = process.platform; // Test on macOS (should be Homebrew) Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }); const macosResult = detectSourceFromPath('/usr/local/bin/claude'); expect(macosResult).toBe('Homebrew'); // Test on Linux (should be native installer) Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }); const linuxResult = detectSourceFromPath('/usr/local/bin/claude'); expect(linuxResult).toBe('native installer'); // Test on Windows (should fallback to PATH) Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); const windowsResult = detectSourceFromPath('/usr/local/bin/claude'); expect(windowsResult).toBe('PATH'); // Restore original platform Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); }); }); describe('Real-world edge cases', () => { it('should handle complex user scenarios', () => { const edgeCases = [ // User with npm aliased to bun { path: '/Users/test/node_modules/@anthropic-ai/claude-code/cli.js', expected: 'npm' }, // Multiple package managers { path: '/Users/test/.bun/bin/claude', expected: 'Bun' }, { path: '/opt/homebrew/bin/claude', expected: 'Homebrew' }, // Custom installations { path: '/opt/custom/claude/bin/claude', expected: 'PATH' }, { path: '/usr/local/custom/bin/claude', expected: 'PATH' } ]; edgeCases.forEach(({ path, expected }) => { const result = detectSourceFromPath(path); expect(result).toBe(expected); }); }); it('should handle path traversal and normalization', () => { const pathNormalizationTests = [ { input: '/opt/homebrew/bin/../lib/claude', expected: 'Homebrew' }, { input: '/Users/test/.bun/bin/./claude', expected: 'Bun' }, { input: 'C:/Users/test/../test/AppData/Local/Claude/claude.exe', expected: 'native installer' } ]; pathNormalizationTests.forEach(({ input, expected }) => { const result = detectSourceFromPath(input); expect(result).toBe(expected); }); }); }); }); describe('CONSORTIUM_CLAUDE_PATH env var', () => { const testClaudePath = '/tmp/test-claude-path'; beforeEach(() => { // Create mock executable fs.writeFileSync(testClaudePath, '#!/bin/bash\necho "mock"'); fs.chmodSync(testClaudePath, 0o755); }); afterEach(() => { if (fs.existsSync(testClaudePath)) fs.unlinkSync(testClaudePath); delete process.env.CONSORTIUM_CLAUDE_PATH; }); it('should use CONSORTIUM_CLAUDE_PATH when set', () => { process.env.CONSORTIUM_CLAUDE_PATH = testClaudePath; const result = findGlobalClaudeCliPath(); expect(result?.source).toBe('CONSORTIUM_CLAUDE_PATH'); // Use realpathSync to handle macOS symlink (/tmp -> /private/tmp) expect(fs.realpathSync(result?.path ?? '')).toBe(fs.realpathSync(testClaudePath)); }); it('should fall back to auto-discovery when env var not set', () => { const result = findGlobalClaudeCliPath(); expect(result?.source).not.toBe('CONSORTIUM_CLAUDE_PATH'); }); it('should ignore env var if path does not exist', () => { process.env.CONSORTIUM_CLAUDE_PATH = '/nonexistent/path/claude'; const result = findGlobalClaudeCliPath(); expect(result?.source).not.toBe('CONSORTIUM_CLAUDE_PATH'); }); });