feat: cross-platform support with Node.js scripts

- Rewrite all bash hooks to Node.js for Windows/macOS/Linux compatibility
- Add package manager auto-detection (npm, pnpm, yarn, bun)
- Add scripts/lib/ with cross-platform utilities
- Add /setup-pm command for package manager configuration
- Add comprehensive test suite (62 tests)

Co-authored-by: zerx-lab
This commit is contained in:
zerx-lab
2026-01-23 15:08:07 +08:00
committed by GitHub
parent 4ec7a6b15a
commit 970f8bf884
16 changed files with 2443 additions and 17 deletions

316
tests/hooks/hooks.test.js Normal file
View File

@@ -0,0 +1,316 @@
/**
* Tests for hook scripts
*
* Run with: node tests/hooks/hooks.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execSync, spawn } = require('child_process');
// 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;
}
}
// Async test helper
async function asyncTest(name, fn) {
try {
await fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
// Run a script and capture output
function runScript(scriptPath, input = '', env = {}) {
return new Promise((resolve, reject) => {
const proc = spawn('node', [scriptPath], {
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout.on('data', data => stdout += data);
proc.stderr.on('data', data => stderr += data);
if (input) {
proc.stdin.write(input);
}
proc.stdin.end();
proc.on('close', code => {
resolve({ code, stdout, stderr });
});
proc.on('error', reject);
});
}
// Create a temporary test directory
function createTestDir() {
const testDir = path.join(os.tmpdir(), `hooks-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
async function runTests() {
console.log('\n=== Testing Hook Scripts ===\n');
let passed = 0;
let failed = 0;
const scriptsDir = path.join(__dirname, '..', '..', 'scripts', 'hooks');
// session-start.js tests
console.log('session-start.js:');
if (await asyncTest('runs without error', async () => {
const result = await runScript(path.join(scriptsDir, 'session-start.js'));
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('outputs session info to stderr', async () => {
const result = await runScript(path.join(scriptsDir, 'session-start.js'));
assert.ok(
result.stderr.includes('[SessionStart]') ||
result.stderr.includes('Package manager'),
'Should output session info'
);
})) passed++; else failed++;
// session-end.js tests
console.log('\nsession-end.js:');
if (await asyncTest('runs without error', async () => {
const result = await runScript(path.join(scriptsDir, 'session-end.js'));
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('creates or updates session file', async () => {
// Run the script
await runScript(path.join(scriptsDir, 'session-end.js'));
// Check if session file was created
const sessionsDir = path.join(os.homedir(), '.claude', 'sessions');
const today = new Date().toISOString().split('T')[0];
const sessionFile = path.join(sessionsDir, `${today}-session.tmp`);
assert.ok(fs.existsSync(sessionFile), 'Session file should exist');
})) passed++; else failed++;
// pre-compact.js tests
console.log('\npre-compact.js:');
if (await asyncTest('runs without error', async () => {
const result = await runScript(path.join(scriptsDir, 'pre-compact.js'));
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('outputs PreCompact message', async () => {
const result = await runScript(path.join(scriptsDir, 'pre-compact.js'));
assert.ok(result.stderr.includes('[PreCompact]'), 'Should output PreCompact message');
})) passed++; else failed++;
if (await asyncTest('creates compaction log', async () => {
await runScript(path.join(scriptsDir, 'pre-compact.js'));
const logFile = path.join(os.homedir(), '.claude', 'sessions', 'compaction-log.txt');
assert.ok(fs.existsSync(logFile), 'Compaction log should exist');
})) passed++; else failed++;
// suggest-compact.js tests
console.log('\nsuggest-compact.js:');
if (await asyncTest('runs without error', async () => {
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
CLAUDE_SESSION_ID: 'test-session-' + Date.now()
});
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('increments counter on each call', async () => {
const sessionId = 'test-counter-' + Date.now();
// Run multiple times
for (let i = 0; i < 3; i++) {
await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
CLAUDE_SESSION_ID: sessionId
});
}
// Check counter file
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
assert.strictEqual(count, 3, `Counter should be 3, got ${count}`);
// Cleanup
fs.unlinkSync(counterFile);
})) passed++; else failed++;
if (await asyncTest('suggests compact at threshold', async () => {
const sessionId = 'test-threshold-' + Date.now();
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
// Set counter to threshold - 1
fs.writeFileSync(counterFile, '49');
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), '', {
CLAUDE_SESSION_ID: sessionId,
COMPACT_THRESHOLD: '50'
});
assert.ok(
result.stderr.includes('50 tool calls reached'),
'Should suggest compact at threshold'
);
// Cleanup
fs.unlinkSync(counterFile);
})) passed++; else failed++;
// evaluate-session.js tests
console.log('\nevaluate-session.js:');
if (await asyncTest('runs without error when no transcript', async () => {
const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'));
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
})) passed++; else failed++;
if (await asyncTest('skips short sessions', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'transcript.jsonl');
// Create a short transcript (less than 10 user messages)
const transcript = Array(5).fill('{"type":"user","content":"test"}\n').join('');
fs.writeFileSync(transcriptPath, transcript);
const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'), '', {
CLAUDE_TRANSCRIPT_PATH: transcriptPath
});
assert.ok(
result.stderr.includes('Session too short'),
'Should indicate session is too short'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
if (await asyncTest('processes sessions with enough messages', async () => {
const testDir = createTestDir();
const transcriptPath = path.join(testDir, 'transcript.jsonl');
// Create a longer transcript (more than 10 user messages)
const transcript = Array(15).fill('{"type":"user","content":"test"}\n').join('');
fs.writeFileSync(transcriptPath, transcript);
const result = await runScript(path.join(scriptsDir, 'evaluate-session.js'), '', {
CLAUDE_TRANSCRIPT_PATH: transcriptPath
});
assert.ok(
result.stderr.includes('15 messages'),
'Should report message count'
);
cleanupTestDir(testDir);
})) passed++; else failed++;
// hooks.json validation
console.log('\nhooks.json Validation:');
if (test('hooks.json is valid JSON', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const content = fs.readFileSync(hooksPath, 'utf8');
JSON.parse(content); // Will throw if invalid
})) passed++; else failed++;
if (test('hooks.json has required event types', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
assert.ok(hooks.hooks.PreToolUse, 'Should have PreToolUse hooks');
assert.ok(hooks.hooks.PostToolUse, 'Should have PostToolUse hooks');
assert.ok(hooks.hooks.SessionStart, 'Should have SessionStart hooks');
assert.ok(hooks.hooks.Stop, 'Should have Stop hooks');
assert.ok(hooks.hooks.PreCompact, 'Should have PreCompact hooks');
})) passed++; else failed++;
if (test('all hook commands use node', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
const checkHooks = (hookArray) => {
for (const entry of hookArray) {
for (const hook of entry.hooks) {
if (hook.type === 'command') {
assert.ok(
hook.command.startsWith('node'),
`Hook command should start with 'node': ${hook.command.substring(0, 50)}...`
);
}
}
}
};
for (const [eventType, hookArray] of Object.entries(hooks.hooks)) {
checkHooks(hookArray);
}
})) passed++; else failed++;
if (test('script references use CLAUDE_PLUGIN_ROOT variable', () => {
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
const checkHooks = (hookArray) => {
for (const entry of hookArray) {
for (const hook of entry.hooks) {
if (hook.type === 'command' && hook.command.includes('scripts/hooks/')) {
// Check for the literal string "${CLAUDE_PLUGIN_ROOT}" in the command
const hasPluginRoot = hook.command.includes('${CLAUDE_PLUGIN_ROOT}');
assert.ok(
hasPluginRoot,
`Script paths should use CLAUDE_PLUGIN_ROOT: ${hook.command.substring(0, 80)}...`
);
}
}
}
};
for (const [eventType, hookArray] of Object.entries(hooks.hooks)) {
checkHooks(hookArray);
}
})) 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();

View File

@@ -0,0 +1,352 @@
/**
* 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');
const utils = require('../../scripts/lib/utils');
// 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('Available package managers'), 'Should list available managers');
assert.ok(prompt.includes('CLAUDE_PACKAGE_MANAGER'), 'Should mention env var');
})) 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();

236
tests/lib/utils.test.js Normal file
View File

@@ -0,0 +1,236 @@
/**
* Tests for scripts/lib/utils.js
*
* Run with: node tests/lib/utils.test.js
*/
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
// Import the module
const utils = require('../../scripts/lib/utils');
// 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;
}
}
// Test suite
function runTests() {
console.log('\n=== Testing utils.js ===\n');
let passed = 0;
let failed = 0;
// Platform detection tests
console.log('Platform Detection:');
if (test('isWindows/isMacOS/isLinux are booleans', () => {
assert.strictEqual(typeof utils.isWindows, 'boolean');
assert.strictEqual(typeof utils.isMacOS, 'boolean');
assert.strictEqual(typeof utils.isLinux, 'boolean');
})) passed++; else failed++;
if (test('exactly one platform should be true', () => {
const platforms = [utils.isWindows, utils.isMacOS, utils.isLinux];
const trueCount = platforms.filter(p => p).length;
// Note: Could be 0 on other platforms like FreeBSD
assert.ok(trueCount <= 1, 'More than one platform is true');
})) passed++; else failed++;
// Directory functions tests
console.log('\nDirectory Functions:');
if (test('getHomeDir returns valid path', () => {
const home = utils.getHomeDir();
assert.strictEqual(typeof home, 'string');
assert.ok(home.length > 0, 'Home dir should not be empty');
assert.ok(fs.existsSync(home), 'Home dir should exist');
})) passed++; else failed++;
if (test('getClaudeDir returns path under home', () => {
const claudeDir = utils.getClaudeDir();
const homeDir = utils.getHomeDir();
assert.ok(claudeDir.startsWith(homeDir), 'Claude dir should be under home');
assert.ok(claudeDir.includes('.claude'), 'Should contain .claude');
})) passed++; else failed++;
if (test('getSessionsDir returns path under Claude dir', () => {
const sessionsDir = utils.getSessionsDir();
const claudeDir = utils.getClaudeDir();
assert.ok(sessionsDir.startsWith(claudeDir), 'Sessions should be under Claude dir');
assert.ok(sessionsDir.includes('sessions'), 'Should contain sessions');
})) passed++; else failed++;
if (test('getTempDir returns valid temp directory', () => {
const tempDir = utils.getTempDir();
assert.strictEqual(typeof tempDir, 'string');
assert.ok(tempDir.length > 0, 'Temp dir should not be empty');
})) passed++; else failed++;
if (test('ensureDir creates directory', () => {
const testDir = path.join(utils.getTempDir(), `utils-test-${Date.now()}`);
try {
utils.ensureDir(testDir);
assert.ok(fs.existsSync(testDir), 'Directory should be created');
} finally {
fs.rmSync(testDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// Date/Time functions tests
console.log('\nDate/Time Functions:');
if (test('getDateString returns YYYY-MM-DD format', () => {
const date = utils.getDateString();
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(date), `Expected YYYY-MM-DD, got ${date}`);
})) passed++; else failed++;
if (test('getTimeString returns HH:MM format', () => {
const time = utils.getTimeString();
assert.ok(/^\d{2}:\d{2}$/.test(time), `Expected HH:MM, got ${time}`);
})) passed++; else failed++;
if (test('getDateTimeString returns full datetime format', () => {
const dt = utils.getDateTimeString();
assert.ok(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dt), `Expected YYYY-MM-DD HH:MM:SS, got ${dt}`);
})) passed++; else failed++;
// File operations tests
console.log('\nFile Operations:');
if (test('readFile returns null for non-existent file', () => {
const content = utils.readFile('/non/existent/file/path.txt');
assert.strictEqual(content, null);
})) passed++; else failed++;
if (test('writeFile and readFile work together', () => {
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
const testContent = 'Hello, World!';
try {
utils.writeFile(testFile, testContent);
const read = utils.readFile(testFile);
assert.strictEqual(read, testContent);
} finally {
fs.unlinkSync(testFile);
}
})) passed++; else failed++;
if (test('appendFile adds content to file', () => {
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
try {
utils.writeFile(testFile, 'Line 1\n');
utils.appendFile(testFile, 'Line 2\n');
const content = utils.readFile(testFile);
assert.strictEqual(content, 'Line 1\nLine 2\n');
} finally {
fs.unlinkSync(testFile);
}
})) passed++; else failed++;
if (test('replaceInFile replaces text', () => {
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
try {
utils.writeFile(testFile, 'Hello, World!');
utils.replaceInFile(testFile, /World/, 'Universe');
const content = utils.readFile(testFile);
assert.strictEqual(content, 'Hello, Universe!');
} finally {
fs.unlinkSync(testFile);
}
})) passed++; else failed++;
if (test('countInFile counts occurrences', () => {
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
try {
utils.writeFile(testFile, 'foo bar foo baz foo');
const count = utils.countInFile(testFile, /foo/g);
assert.strictEqual(count, 3);
} finally {
fs.unlinkSync(testFile);
}
})) passed++; else failed++;
if (test('grepFile finds matching lines', () => {
const testFile = path.join(utils.getTempDir(), `utils-test-${Date.now()}.txt`);
try {
utils.writeFile(testFile, 'line 1 foo\nline 2 bar\nline 3 foo');
const matches = utils.grepFile(testFile, /foo/);
assert.strictEqual(matches.length, 2);
assert.strictEqual(matches[0].lineNumber, 1);
assert.strictEqual(matches[1].lineNumber, 3);
} finally {
fs.unlinkSync(testFile);
}
})) passed++; else failed++;
// findFiles tests
console.log('\nfindFiles:');
if (test('findFiles returns empty for non-existent directory', () => {
const results = utils.findFiles('/non/existent/dir', '*.txt');
assert.strictEqual(results.length, 0);
})) passed++; else failed++;
if (test('findFiles finds matching files', () => {
const testDir = path.join(utils.getTempDir(), `utils-test-${Date.now()}`);
try {
fs.mkdirSync(testDir);
fs.writeFileSync(path.join(testDir, 'test1.txt'), 'content');
fs.writeFileSync(path.join(testDir, 'test2.txt'), 'content');
fs.writeFileSync(path.join(testDir, 'test.md'), 'content');
const txtFiles = utils.findFiles(testDir, '*.txt');
assert.strictEqual(txtFiles.length, 2);
const mdFiles = utils.findFiles(testDir, '*.md');
assert.strictEqual(mdFiles.length, 1);
} finally {
fs.rmSync(testDir, { recursive: true });
}
})) passed++; else failed++;
// System functions tests
console.log('\nSystem Functions:');
if (test('commandExists finds node', () => {
const exists = utils.commandExists('node');
assert.strictEqual(exists, true);
})) passed++; else failed++;
if (test('commandExists returns false for fake command', () => {
const exists = utils.commandExists('nonexistent_command_12345');
assert.strictEqual(exists, false);
})) passed++; else failed++;
if (test('runCommand executes simple command', () => {
const result = utils.runCommand('node --version');
assert.strictEqual(result.success, true);
assert.ok(result.output.startsWith('v'), 'Should start with v');
})) passed++; else failed++;
if (test('runCommand handles failed command', () => {
const result = utils.runCommand('node --invalid-flag-12345');
assert.strictEqual(result.success, false);
})) 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();

76
tests/run-all.js Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Run all tests
*
* Usage: node tests/run-all.js
*/
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const testsDir = __dirname;
const testFiles = [
'lib/utils.test.js',
'lib/package-manager.test.js',
'hooks/hooks.test.js'
];
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ Everything Claude Code - Test Suite ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log();
let totalPassed = 0;
let totalFailed = 0;
let totalTests = 0;
for (const testFile of testFiles) {
const testPath = path.join(testsDir, testFile);
if (!fs.existsSync(testPath)) {
console.log(`⚠ Skipping ${testFile} (file not found)`);
continue;
}
console.log(`\n━━━ Running ${testFile} ━━━`);
try {
const output = execSync(`node "${testPath}"`, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
console.log(output);
// Parse results from output
const passedMatch = output.match(/Passed:\s*(\d+)/);
const failedMatch = output.match(/Failed:\s*(\d+)/);
if (passedMatch) totalPassed += parseInt(passedMatch[1], 10);
if (failedMatch) totalFailed += parseInt(failedMatch[1], 10);
} catch (err) {
console.log(err.stdout || '');
console.log(err.stderr || '');
// Parse results even on failure
const output = (err.stdout || '') + (err.stderr || '');
const passedMatch = output.match(/Passed:\s*(\d+)/);
const failedMatch = output.match(/Failed:\s*(\d+)/);
if (passedMatch) totalPassed += parseInt(passedMatch[1], 10);
if (failedMatch) totalFailed += parseInt(failedMatch[1], 10);
}
}
totalTests = totalPassed + totalFailed;
console.log('\n╔══════════════════════════════════════════════════════════╗');
console.log('║ Final Results ║');
console.log('╠══════════════════════════════════════════════════════════╣');
console.log(`║ Total Tests: ${String(totalTests).padStart(4)}`);
console.log(`║ Passed: ${String(totalPassed).padStart(4)} ✓ ║`);
console.log(`║ Failed: ${String(totalFailed).padStart(4)} ${totalFailed > 0 ? '✗' : ' '}`);
console.log('╚══════════════════════════════════════════════════════════╝');
process.exit(totalFailed > 0 ? 1 : 0);