mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-17 19:53:09 +08:00
Validate args parameter in getExecCommand() against SAFE_ARGS_REGEX to prevent command injection when returned string is passed to a shell. Escape regex metacharacters in getCommandPattern() generic action branch to prevent malformed patterns and unintended matching. Clean up stdin listeners in readStdinJson() timeout path to prevent process hanging.
943 lines
34 KiB
JavaScript
943 lines
34 KiB
JavaScript
/**
|
|
* Tests for scripts/lib/package-manager.js
|
|
*
|
|
* Run with: node tests/lib/package-manager.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
// Import the modules
|
|
const pm = require('../../scripts/lib/package-manager');
|
|
|
|
// Test helper
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create a temporary test directory
|
|
function createTestDir() {
|
|
const testDir = path.join(os.tmpdir(), `pm-test-${Date.now()}`);
|
|
fs.mkdirSync(testDir, { recursive: true });
|
|
return testDir;
|
|
}
|
|
|
|
// Clean up test directory
|
|
function cleanupTestDir(testDir) {
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
|
|
// Test suite
|
|
function runTests() {
|
|
console.log('\n=== Testing package-manager.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
// PACKAGE_MANAGERS constant tests
|
|
console.log('PACKAGE_MANAGERS Constant:');
|
|
|
|
if (test('PACKAGE_MANAGERS has all expected managers', () => {
|
|
assert.ok(pm.PACKAGE_MANAGERS.npm, 'Should have npm');
|
|
assert.ok(pm.PACKAGE_MANAGERS.pnpm, 'Should have pnpm');
|
|
assert.ok(pm.PACKAGE_MANAGERS.yarn, 'Should have yarn');
|
|
assert.ok(pm.PACKAGE_MANAGERS.bun, 'Should have bun');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('Each manager has required properties', () => {
|
|
const requiredProps = ['name', 'lockFile', 'installCmd', 'runCmd', 'execCmd', 'testCmd', 'buildCmd', 'devCmd'];
|
|
for (const [name, config] of Object.entries(pm.PACKAGE_MANAGERS)) {
|
|
for (const prop of requiredProps) {
|
|
assert.ok(config[prop], `${name} should have ${prop}`);
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// detectFromLockFile tests
|
|
console.log('\ndetectFromLockFile:');
|
|
|
|
if (test('detects npm from package-lock.json', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}');
|
|
const result = pm.detectFromLockFile(testDir);
|
|
assert.strictEqual(result, 'npm');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('detects pnpm from pnpm-lock.yaml', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'pnpm-lock.yaml'), '');
|
|
const result = pm.detectFromLockFile(testDir);
|
|
assert.strictEqual(result, 'pnpm');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('detects yarn from yarn.lock', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'yarn.lock'), '');
|
|
const result = pm.detectFromLockFile(testDir);
|
|
assert.strictEqual(result, 'yarn');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('detects bun from bun.lockb', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'bun.lockb'), '');
|
|
const result = pm.detectFromLockFile(testDir);
|
|
assert.strictEqual(result, 'bun');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns null when no lock file exists', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
const result = pm.detectFromLockFile(testDir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('respects detection priority (pnpm > npm)', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
// Create both lock files
|
|
fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}');
|
|
fs.writeFileSync(path.join(testDir, 'pnpm-lock.yaml'), '');
|
|
const result = pm.detectFromLockFile(testDir);
|
|
// pnpm has higher priority in DETECTION_PRIORITY
|
|
assert.strictEqual(result, 'pnpm');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// detectFromPackageJson tests
|
|
console.log('\ndetectFromPackageJson:');
|
|
|
|
if (test('detects package manager from packageManager field', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
|
|
name: 'test',
|
|
packageManager: 'pnpm@8.6.0'
|
|
}));
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, 'pnpm');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('handles packageManager without version', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
|
|
name: 'test',
|
|
packageManager: 'yarn'
|
|
}));
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, 'yarn');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns null when no packageManager field', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
|
|
name: 'test'
|
|
}));
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns null when no package.json exists', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getAvailablePackageManagers tests
|
|
console.log('\ngetAvailablePackageManagers:');
|
|
|
|
if (test('returns array of available managers', () => {
|
|
const available = pm.getAvailablePackageManagers();
|
|
assert.ok(Array.isArray(available), 'Should return array');
|
|
// npm should always be available with Node.js
|
|
assert.ok(available.includes('npm'), 'npm should be available');
|
|
})) passed++; else failed++;
|
|
|
|
// getPackageManager tests
|
|
console.log('\ngetPackageManager:');
|
|
|
|
if (test('returns object with name, config, and source', () => {
|
|
const result = pm.getPackageManager();
|
|
assert.ok(result.name, 'Should have name');
|
|
assert.ok(result.config, 'Should have config');
|
|
assert.ok(result.source, 'Should have source');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('respects environment variable', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
|
const result = pm.getPackageManager();
|
|
assert.strictEqual(result.name, 'yarn');
|
|
assert.strictEqual(result.source, 'environment');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('detects from lock file in project', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'bun.lockb'), '');
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.strictEqual(result.name, 'bun');
|
|
assert.strictEqual(result.source, 'lock-file');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getRunCommand tests
|
|
console.log('\ngetRunCommand:');
|
|
|
|
if (test('returns correct install command', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
|
const cmd = pm.getRunCommand('install');
|
|
assert.strictEqual(cmd, 'pnpm install');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns correct test command', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getRunCommand('test');
|
|
assert.strictEqual(cmd, 'npm test');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getExecCommand tests
|
|
console.log('\ngetExecCommand:');
|
|
|
|
if (test('returns correct exec command for npm', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getExecCommand('prettier', '--write .');
|
|
assert.strictEqual(cmd, 'npx prettier --write .');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns correct exec command for pnpm', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
|
const cmd = pm.getExecCommand('eslint', '.');
|
|
assert.strictEqual(cmd, 'pnpm dlx eslint .');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getCommandPattern tests
|
|
console.log('\ngetCommandPattern:');
|
|
|
|
if (test('generates pattern for dev command', () => {
|
|
const pattern = pm.getCommandPattern('dev');
|
|
assert.ok(pattern.includes('npm run dev'), 'Should include npm');
|
|
assert.ok(pattern.includes('pnpm'), 'Should include pnpm');
|
|
assert.ok(pattern.includes('yarn dev'), 'Should include yarn');
|
|
assert.ok(pattern.includes('bun run dev'), 'Should include bun');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('pattern matches actual commands', () => {
|
|
const pattern = pm.getCommandPattern('test');
|
|
const regex = new RegExp(pattern);
|
|
|
|
assert.ok(regex.test('npm test'), 'Should match npm test');
|
|
assert.ok(regex.test('pnpm test'), 'Should match pnpm test');
|
|
assert.ok(regex.test('yarn test'), 'Should match yarn test');
|
|
assert.ok(regex.test('bun test'), 'Should match bun test');
|
|
assert.ok(!regex.test('cargo test'), 'Should not match cargo test');
|
|
})) passed++; else failed++;
|
|
|
|
// getSelectionPrompt tests
|
|
console.log('\ngetSelectionPrompt:');
|
|
|
|
if (test('returns informative prompt', () => {
|
|
const prompt = pm.getSelectionPrompt();
|
|
assert.ok(prompt.includes('Supported package managers'), 'Should list supported managers');
|
|
assert.ok(prompt.includes('CLAUDE_PACKAGE_MANAGER'), 'Should mention env var');
|
|
assert.ok(prompt.includes('lock file'), 'Should mention lock file option');
|
|
})) passed++; else failed++;
|
|
|
|
// setProjectPackageManager tests
|
|
console.log('\nsetProjectPackageManager:');
|
|
|
|
if (test('sets project package manager', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
const result = pm.setProjectPackageManager('pnpm', testDir);
|
|
assert.strictEqual(result.packageManager, 'pnpm');
|
|
assert.ok(result.setAt, 'Should have setAt timestamp');
|
|
|
|
// Verify file was created
|
|
const configPath = path.join(testDir, '.claude', 'package-manager.json');
|
|
assert.ok(fs.existsSync(configPath), 'Config file should exist');
|
|
const saved = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
assert.strictEqual(saved.packageManager, 'pnpm');
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects unknown package manager', () => {
|
|
assert.throws(() => {
|
|
pm.setProjectPackageManager('cargo');
|
|
}, /Unknown package manager/);
|
|
})) passed++; else failed++;
|
|
|
|
// setPreferredPackageManager tests
|
|
console.log('\nsetPreferredPackageManager:');
|
|
|
|
if (test('rejects unknown package manager', () => {
|
|
assert.throws(() => {
|
|
pm.setPreferredPackageManager('pip');
|
|
}, /Unknown package manager/);
|
|
})) passed++; else failed++;
|
|
|
|
// detectFromPackageJson edge cases
|
|
console.log('\ndetectFromPackageJson (edge cases):');
|
|
|
|
if (test('handles invalid JSON in package.json', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package.json'), 'NOT VALID JSON');
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns null for unknown package manager in packageManager field', () => {
|
|
const testDir = createTestDir();
|
|
try {
|
|
fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({
|
|
name: 'test',
|
|
packageManager: 'deno@1.0'
|
|
}));
|
|
const result = pm.detectFromPackageJson(testDir);
|
|
assert.strictEqual(result, null);
|
|
} finally {
|
|
cleanupTestDir(testDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getExecCommand edge cases
|
|
console.log('\ngetExecCommand (edge cases):');
|
|
|
|
if (test('returns exec command without args', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getExecCommand('prettier');
|
|
assert.strictEqual(cmd, 'npx prettier');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getRunCommand additional cases
|
|
console.log('\ngetRunCommand (additional):');
|
|
|
|
if (test('returns correct build command', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
assert.strictEqual(pm.getRunCommand('build'), 'npm run build');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns correct dev command', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
assert.strictEqual(pm.getRunCommand('dev'), 'npm run dev');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('returns correct custom script command', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
assert.strictEqual(pm.getRunCommand('lint'), 'npm run lint');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// DETECTION_PRIORITY tests
|
|
console.log('\nDETECTION_PRIORITY:');
|
|
|
|
if (test('has pnpm first', () => {
|
|
assert.strictEqual(pm.DETECTION_PRIORITY[0], 'pnpm');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('has npm last', () => {
|
|
assert.strictEqual(pm.DETECTION_PRIORITY[pm.DETECTION_PRIORITY.length - 1], 'npm');
|
|
})) passed++; else failed++;
|
|
|
|
// getCommandPattern additional cases
|
|
console.log('\ngetCommandPattern (additional):');
|
|
|
|
if (test('generates pattern for install command', () => {
|
|
const pattern = pm.getCommandPattern('install');
|
|
const regex = new RegExp(pattern);
|
|
assert.ok(regex.test('npm install'), 'Should match npm install');
|
|
assert.ok(regex.test('pnpm install'), 'Should match pnpm install');
|
|
assert.ok(regex.test('yarn'), 'Should match yarn (install implicit)');
|
|
assert.ok(regex.test('bun install'), 'Should match bun install');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('generates pattern for custom action', () => {
|
|
const pattern = pm.getCommandPattern('lint');
|
|
const regex = new RegExp(pattern);
|
|
assert.ok(regex.test('npm run lint'), 'Should match npm run lint');
|
|
assert.ok(regex.test('pnpm lint'), 'Should match pnpm lint');
|
|
assert.ok(regex.test('yarn lint'), 'Should match yarn lint');
|
|
assert.ok(regex.test('bun run lint'), 'Should match bun run lint');
|
|
})) passed++; else failed++;
|
|
|
|
// getPackageManager robustness tests
|
|
console.log('\ngetPackageManager (robustness):');
|
|
|
|
if (test('falls through on corrupted project config JSON', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-robust-'));
|
|
const claudeDir = path.join(testDir, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), '{not valid json!!!');
|
|
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
// Should fall through to default (npm) since project config is corrupt
|
|
assert.ok(result.name, 'Should return a package manager');
|
|
assert.ok(result.source !== 'project-config', 'Should not use corrupt project config');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('falls through on project config with unknown PM', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-robust-'));
|
|
const claudeDir = path.join(testDir, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
fs.writeFileSync(path.join(claudeDir, 'package-manager.json'),
|
|
JSON.stringify({ packageManager: 'nonexistent-pm' }));
|
|
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.ok(result.name, 'Should return a package manager');
|
|
assert.ok(result.source !== 'project-config', 'Should not use unknown PM config');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getRunCommand validation tests
|
|
console.log('\ngetRunCommand (validation):');
|
|
|
|
if (test('rejects empty script name', () => {
|
|
assert.throws(() => pm.getRunCommand(''), /non-empty string/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects null script name', () => {
|
|
assert.throws(() => pm.getRunCommand(null), /non-empty string/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects script name with shell metacharacters', () => {
|
|
assert.throws(() => pm.getRunCommand('test; rm -rf /'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects script name with backticks', () => {
|
|
assert.throws(() => pm.getRunCommand('test`whoami`'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('accepts scoped package names', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getRunCommand('@scope/my-script');
|
|
assert.strictEqual(cmd, 'npm run @scope/my-script');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getExecCommand validation tests
|
|
console.log('\ngetExecCommand (validation):');
|
|
|
|
if (test('rejects empty binary name', () => {
|
|
assert.throws(() => pm.getExecCommand(''), /non-empty string/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects null binary name', () => {
|
|
assert.throws(() => pm.getExecCommand(null), /non-empty string/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects binary name with shell metacharacters', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier; cat /etc/passwd'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('accepts dotted binary names like tsc', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getExecCommand('tsc');
|
|
assert.strictEqual(cmd, 'npx tsc');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getPackageManager source detection tests
|
|
console.log('\ngetPackageManager (source detection):');
|
|
|
|
if (test('detects from valid project-config (.claude/package-manager.json)', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-projcfg-'));
|
|
const claudeDir = path.join(testDir, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
fs.writeFileSync(path.join(claudeDir, 'package-manager.json'),
|
|
JSON.stringify({ packageManager: 'pnpm' }));
|
|
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.strictEqual(result.name, 'pnpm', 'Should detect pnpm from project config');
|
|
assert.strictEqual(result.source, 'project-config', 'Source should be project-config');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('project-config takes priority over package.json', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-priority-'));
|
|
const claudeDir = path.join(testDir, '.claude');
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
|
|
// Project config says bun
|
|
fs.writeFileSync(path.join(claudeDir, 'package-manager.json'),
|
|
JSON.stringify({ packageManager: 'bun' }));
|
|
// package.json says yarn
|
|
fs.writeFileSync(path.join(testDir, 'package.json'),
|
|
JSON.stringify({ packageManager: 'yarn@4.0.0' }));
|
|
// Lock file says npm
|
|
fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}');
|
|
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.strictEqual(result.name, 'bun', 'Project config should win over package.json and lock file');
|
|
assert.strictEqual(result.source, 'project-config');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('package.json takes priority over lock file', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-pj-lock-'));
|
|
// package.json says yarn
|
|
fs.writeFileSync(path.join(testDir, 'package.json'),
|
|
JSON.stringify({ packageManager: 'yarn@4.0.0' }));
|
|
// Lock file says npm
|
|
fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}');
|
|
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.strictEqual(result.name, 'yarn', 'package.json should win over lock file');
|
|
assert.strictEqual(result.source, 'package.json');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('defaults to npm when no config found', () => {
|
|
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-default-'));
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
const result = pm.getPackageManager({ projectDir: testDir });
|
|
assert.strictEqual(result.name, 'npm', 'Should default to npm');
|
|
assert.strictEqual(result.source, 'default');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
}
|
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// setPreferredPackageManager success
|
|
console.log('\nsetPreferredPackageManager (success):');
|
|
|
|
if (test('successfully saves preferred package manager', () => {
|
|
// This writes to ~/.claude/package-manager.json — read original to restore
|
|
const utils = require('../../scripts/lib/utils');
|
|
const configPath = path.join(utils.getClaudeDir(), 'package-manager.json');
|
|
const original = utils.readFile(configPath);
|
|
try {
|
|
const config = pm.setPreferredPackageManager('bun');
|
|
assert.strictEqual(config.packageManager, 'bun');
|
|
assert.ok(config.setAt, 'Should have setAt timestamp');
|
|
// Verify it was persisted
|
|
const saved = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
assert.strictEqual(saved.packageManager, 'bun');
|
|
} finally {
|
|
// Restore original config
|
|
if (original) {
|
|
fs.writeFileSync(configPath, original, 'utf8');
|
|
} else {
|
|
try { fs.unlinkSync(configPath); } catch {}
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getCommandPattern completeness
|
|
console.log('\ngetCommandPattern (completeness):');
|
|
|
|
if (test('generates pattern for test command', () => {
|
|
const pattern = pm.getCommandPattern('test');
|
|
assert.ok(pattern.includes('npm test'), 'Should include npm test');
|
|
assert.ok(pattern.includes('pnpm test'), 'Should include pnpm test');
|
|
assert.ok(pattern.includes('bun test'), 'Should include bun test');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('generates pattern for build command', () => {
|
|
const pattern = pm.getCommandPattern('build');
|
|
assert.ok(pattern.includes('npm run build'), 'Should include npm run build');
|
|
assert.ok(pattern.includes('yarn build'), 'Should include yarn build');
|
|
})) passed++; else failed++;
|
|
|
|
// getRunCommand PM-specific format tests
|
|
console.log('\ngetRunCommand (PM-specific formats):');
|
|
|
|
if (test('pnpm custom script: pnpm <script> (no run keyword)', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
|
const cmd = pm.getRunCommand('lint');
|
|
assert.strictEqual(cmd, 'pnpm lint', 'pnpm uses "pnpm <script>" format');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('yarn custom script: yarn <script>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
|
const cmd = pm.getRunCommand('format');
|
|
assert.strictEqual(cmd, 'yarn format', 'yarn uses "yarn <script>" format');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('bun custom script: bun run <script>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
|
const cmd = pm.getRunCommand('typecheck');
|
|
assert.strictEqual(cmd, 'bun run typecheck', 'bun uses "bun run <script>" format');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('npm custom script: npm run <script>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'npm';
|
|
const cmd = pm.getRunCommand('lint');
|
|
assert.strictEqual(cmd, 'npm run lint', 'npm uses "npm run <script>" format');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('pnpm install returns pnpm install', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
|
assert.strictEqual(pm.getRunCommand('install'), 'pnpm install');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('yarn install returns yarn (no install keyword)', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
|
assert.strictEqual(pm.getRunCommand('install'), 'yarn');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('bun test returns bun test', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
|
assert.strictEqual(pm.getRunCommand('test'), 'bun test');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// getExecCommand PM-specific format tests
|
|
console.log('\ngetExecCommand (PM-specific formats):');
|
|
|
|
if (test('pnpm exec: pnpm dlx <binary>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'pnpm';
|
|
assert.strictEqual(pm.getExecCommand('prettier', '--write .'), 'pnpm dlx prettier --write .');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('yarn exec: yarn dlx <binary>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'yarn';
|
|
assert.strictEqual(pm.getExecCommand('eslint', '.'), 'yarn dlx eslint .');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('bun exec: bunx <binary>', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'bun';
|
|
assert.strictEqual(pm.getExecCommand('tsc', '--noEmit'), 'bunx tsc --noEmit');
|
|
} finally {
|
|
if (originalEnv !== undefined) process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
else delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (test('ignores unknown env var package manager', () => {
|
|
const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER;
|
|
try {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = 'totally-fake-pm';
|
|
const result = pm.getPackageManager();
|
|
// Should ignore invalid env var and fall through
|
|
assert.notStrictEqual(result.name, 'totally-fake-pm', 'Should not use unknown PM');
|
|
} finally {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CLAUDE_PACKAGE_MANAGER = originalEnv;
|
|
} else {
|
|
delete process.env.CLAUDE_PACKAGE_MANAGER;
|
|
}
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
// ─── Round 21: getExecCommand args validation ───
|
|
console.log('\ngetExecCommand (args validation):');
|
|
|
|
if (test('rejects args with shell metacharacter semicolon', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier', '; rm -rf /'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects args with pipe character', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier', '--write . | cat'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects args with backtick injection', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier', '`whoami`'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects args with dollar sign', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier', '$HOME'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('rejects args with ampersand', () => {
|
|
assert.throws(() => pm.getExecCommand('prettier', '--write . && echo pwned'), /unsafe characters/);
|
|
})) passed++; else failed++;
|
|
|
|
if (test('allows safe args like --write .', () => {
|
|
const cmd = pm.getExecCommand('prettier', '--write .');
|
|
assert.ok(cmd.includes('--write .'), 'Should include safe args');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('allows empty args without trailing space', () => {
|
|
const cmd = pm.getExecCommand('prettier', '');
|
|
assert.ok(!cmd.endsWith(' '), 'Should not have trailing space for empty args');
|
|
})) passed++; else failed++;
|
|
|
|
// ─── Round 21: getCommandPattern regex escaping ───
|
|
console.log('\ngetCommandPattern (regex escaping):');
|
|
|
|
if (test('escapes dot in action name for regex safety', () => {
|
|
const pattern = pm.getCommandPattern('test.all');
|
|
// The dot should be escaped to \\. in the pattern
|
|
const regex = new RegExp(pattern);
|
|
assert.ok(regex.test('npm run test.all'), 'Should match literal dot');
|
|
assert.ok(!regex.test('npm run testXall'), 'Should NOT match arbitrary character in place of dot');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('escapes brackets in action name', () => {
|
|
const pattern = pm.getCommandPattern('build[prod]');
|
|
const regex = new RegExp(pattern);
|
|
assert.ok(regex.test('npm run build[prod]'), 'Should match literal brackets');
|
|
})) passed++; else failed++;
|
|
|
|
if (test('escapes parentheses in action name', () => {
|
|
// Should not throw when compiled as regex
|
|
const pattern = pm.getCommandPattern('foo(bar)');
|
|
assert.doesNotThrow(() => new RegExp(pattern), 'Should produce valid regex with escaped parens');
|
|
})) passed++; else failed++;
|
|
|
|
// Summary
|
|
console.log('\n=== Test Results ===');
|
|
console.log(`Passed: ${passed}`);
|
|
console.log(`Failed: ${failed}`);
|
|
console.log(`Total: ${passed + failed}\n`);
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests();
|