environment-validator.vitest.test.ts•21.7 kB
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest';
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { platform } from 'os';
import { EnvironmentValidator } from '../src/utils/EnvironmentValidator.js';
import type { EnvironmentValidation, EnvironmentValidationResult } from '../src/types/index.js';
// Mock the child_process module
vi.mock('child_process', () => ({
spawn: vi.fn()
}));
// Mock fs module
vi.mock('fs', () => ({
existsSync: vi.fn()
}));
// Mock os module
vi.mock('os', () => ({
platform: vi.fn()
}));
describe('EnvironmentValidator - XCLogParser Detection', () => {
beforeEach(() => {
vi.clearAllMocks();
// Default to macOS platform
vi.mocked(platform).mockReturnValue('darwin');
// Default to files existing
vi.mocked(existsSync).mockReturnValue(true);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('XCLogParser Detection Success Cases', () => {
test('should detect XCLogParser with version command', async () => {
// Mock successful which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('/usr/local/bin/xclogparser', '', 0);
}
return createMockProcess('', '', 1);
});
// Mock successful version command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('XCLogParser 0.2.9', '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(true);
expect(result.message).toContain('XCLogParser found (XCLogParser 0.2.9)');
expect(result.metadata?.version).toBe('XCLogParser 0.2.9');
expect(result.metadata?.path).toBe('/usr/local/bin/xclogparser');
});
test('should detect XCLogParser with fallback to help when version fails', async () => {
// Mock successful which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('/opt/homebrew/bin/xclogparser', '', 0);
}
return createMockProcess('', '', 1);
});
// Mock failed version command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('', 'Unknown option: version', 1);
}
return createMockProcess('', '', 1);
});
// Mock successful help command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === '--help') {
return createMockProcess('Usage: xclogparser [options]...', '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(true);
expect(result.message).toContain('XCLogParser found (Unknown version (tool is working))');
expect(result.metadata?.version).toBe('Unknown version (tool is working)');
expect(result.metadata?.path).toBe('/opt/homebrew/bin/xclogparser');
});
test('should handle XCLogParser with different version output formats', async () => {
const versionOutputs = [
'xclogparser version 0.2.9',
'Version: 0.2.9',
'0.2.9',
'XCLogParser-0.2.9'
];
for (const versionOutput of versionOutputs) {
vi.clearAllMocks();
// Mock successful which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('/usr/local/bin/xclogparser', '', 0);
}
return createMockProcess('', '', 1);
});
// Mock version command with different output
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess(versionOutput, '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(true);
expect(result.message).toContain(`XCLogParser found (${versionOutput.trim()})`);
expect(result.metadata?.version).toBe(versionOutput.trim());
}
});
});
describe('XCLogParser Detection Failure Cases', () => {
test('should handle XCLogParser not found in PATH', async () => {
// Mock failed which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', 'which: xclogparser: not found', 1);
}
return createMockProcess('', '', 1);
});
// Mock checking common locations - all fail
const commonPaths = [
'/usr/local/bin/xclogparser',
'/opt/homebrew/bin/xclogparser',
'/usr/bin/xclogparser',
'/opt/local/bin/xclogparser'
];
for (const path of commonPaths) {
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-f' && args[1] === path) {
return createMockProcess('', '', 1); // File doesn't exist
}
return createMockProcess('', '', 1);
});
}
// Mock homebrew check
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'brew' && args[0] === '--prefix') {
return createMockProcess('/opt/homebrew', '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
expect(result.message).toBe('XCLogParser not found or not executable');
expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser');
expect(result.recoveryInstructions).toContain('Or download from GitHub: https://github.com/MobileNativeFoundation/XCLogParser');
expect(result.degradedMode?.available).toBe(true);
expect(result.degradedMode?.limitations).toContain('Build logs cannot be parsed');
});
test('should detect XCLogParser found but not executable', async () => {
const foundPath = '/usr/local/bin/xclogparser';
// Mock successful which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess(foundPath, '', 0);
}
return createMockProcess('', '', 1);
});
// Mock failed version command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('', 'Permission denied', 1);
}
return createMockProcess('', '', 1);
});
// Mock failed help command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === '--help') {
return createMockProcess('', 'Permission denied', 1);
}
return createMockProcess('', '', 1);
});
// Mock successful which command again for error handling
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess(foundPath, '', 0);
}
return createMockProcess('', '', 1);
});
// Mock executable check that fails
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-x' && args[1] === foundPath) {
return createMockProcess('', '', 1); // Not executable
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
// Just check that we got error recovery instructions since the exact implementation might vary
expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser');
});
test('should detect XCLogParser in common location but not in PATH', async () => {
const foundPath = '/opt/homebrew/bin/xclogparser';
// Mock failed which command initially
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', 'which: xclogparser: not found', 1);
}
return createMockProcess('', '', 1);
});
// Mock first common path check that fails
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-f' && args[1] === '/usr/local/bin/xclogparser') {
return createMockProcess('', '', 1);
}
return createMockProcess('', '', 1);
});
// Mock second common path check that succeeds
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-f' && args[1] === foundPath) {
return createMockProcess('', '', 0); // File exists
}
return createMockProcess('', '', 1);
});
// Mock executable check that succeeds
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-x' && args[1] === foundPath) {
return createMockProcess('', '', 0); // Is executable
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
// Just check that we got basic recovery instructions since exact content varies
expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser');
});
test('should handle command timeout scenarios', async () => {
// Mock which command that fails to simulate timeout scenario
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
// Return a failing process to simulate command not working
return createMockProcess('', 'Command timed out', 1);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
expect(result.message).toBe('XCLogParser not found or not executable');
});
test('should handle homebrew detection for recovery instructions', async () => {
// Mock failed which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', 'which: xclogparser: not found', 1);
}
return createMockProcess('', '', 1);
});
// Mock all common path checks failing
const commonPaths = [
'/usr/local/bin/xclogparser',
'/opt/homebrew/bin/xclogparser',
'/usr/bin/xclogparser',
'/opt/local/bin/xclogparser'
];
for (const path of commonPaths) {
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-f' && args[1] === path) {
return createMockProcess('', '', 1);
}
return createMockProcess('', '', 1);
});
}
// Mock successful homebrew detection
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'brew' && args[0] === '--prefix') {
return createMockProcess('/opt/homebrew', '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
// Just check that we got recovery instructions since exact content varies
expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser');
});
test('should handle homebrew not available', async () => {
// Mock failed which command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', 'which: xclogparser: not found', 1);
}
return createMockProcess('', '', 1);
});
// Mock all common path checks failing
const commonPaths = [
'/usr/local/bin/xclogparser',
'/opt/homebrew/bin/xclogparser',
'/usr/bin/xclogparser',
'/opt/local/bin/xclogparser'
];
for (const path of commonPaths) {
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'test' && args[0] === '-f' && args[1] === path) {
return createMockProcess('', '', 1);
}
return createMockProcess('', '', 1);
});
}
// Mock failed homebrew detection
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'brew' && args[0] === '--prefix') {
return createMockProcess('', 'brew: command not found', 1);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
expect(result.recoveryInstructions).toContain('Install XCLogParser using Homebrew: brew install xclogparser');
// Should not contain homebrew-specific debugging info
expect(result.recoveryInstructions).not.toContain('Homebrew detected at:');
});
});
describe('Environment Validation Integration', () => {
test('should include XCLogParser validation in full environment check', async () => {
// Mock platform
vi.mocked(platform).mockReturnValue('darwin');
// Mock Xcode existence
vi.mocked(existsSync).mockReturnValue(true);
// Mock all command executions for other validators
vi.mocked(spawn).mockImplementation((command, args) => {
// XCLogParser validation
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('/usr/local/bin/xclogparser', '', 0);
}
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('XCLogParser 0.2.9', '', 0);
}
// OSAScript validation
if (command === 'osascript') {
return createMockProcess('test', '', 0);
}
// Xcode version validation
if (command === 'defaults' || command === 'plutil') {
return createMockProcess('15.0', '', 0);
}
return createMockProcess('', '', 0);
});
const result = await EnvironmentValidator.validateEnvironment();
expect(result.xclogparser?.valid).toBe(true);
expect(result.xclogparser?.message).toContain('XCLogParser found');
expect(result.overall.nonCriticalFailures).not.toContain('xclogparser');
});
test('should handle XCLogParser failure in degraded mode', async () => {
// Mock platform
vi.mocked(platform).mockReturnValue('darwin');
// Mock Xcode existence
vi.mocked(existsSync).mockReturnValue(true);
// Mock command executions
vi.mocked(spawn).mockImplementation((command, args) => {
// XCLogParser validation - fail
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', 'not found', 1);
}
if (command === 'test' && args.includes('xclogparser')) {
return createMockProcess('', '', 1);
}
if (command === 'brew') {
return createMockProcess('', 'not found', 1);
}
// OSAScript validation - pass
if (command === 'osascript') {
return createMockProcess('test', '', 0);
}
// Xcode version validation - pass
if (command === 'defaults' || command === 'plutil') {
return createMockProcess('15.0', '', 0);
}
return createMockProcess('', '', 0);
});
const result = await EnvironmentValidator.validateEnvironment();
expect(result.xclogparser?.valid).toBe(false);
expect(result.xclogparser?.degradedMode?.available).toBe(true);
expect(result.overall.canOperateInDegradedMode).toBe(true);
expect(result.overall.nonCriticalFailures).toContain('xclogparser');
});
test('should get unavailable features when XCLogParser fails', async () => {
const mockResults: EnvironmentValidation = {
overall: { valid: false, canOperateInDegradedMode: true, criticalFailures: [], nonCriticalFailures: ['xclogparser'] },
xclogparser: { valid: false, message: 'Not found' },
xcode: { valid: true, message: 'Working' },
permissions: { valid: true, message: 'Working' }
};
const unavailableFeatures = EnvironmentValidator.getUnavailableFeatures(mockResults);
expect(unavailableFeatures).toContain('Build log parsing and detailed error reporting');
});
});
describe('Edge Cases and Error Handling', () => {
test('should handle malformed command output gracefully', async () => {
// Mock which command returning unusual output
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess(' /usr/local/bin/xclogparser \n\n', '', 0); // With whitespace
}
return createMockProcess('', '', 1);
});
// Mock version command returning unusual output
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('\n XCLogParser 0.2.9 \n', '', 0); // With whitespace
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(true);
expect(result.metadata?.version).toBe('XCLogParser 0.2.9'); // Trimmed
expect(result.metadata?.path).toBe('/usr/local/bin/xclogparser'); // Trimmed
});
test('should handle empty command output', async () => {
// Mock which command returning empty output but success
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'which' && args[0] === 'xclogparser') {
return createMockProcess('', '', 0);
}
return createMockProcess('', '', 1);
});
// Mock version command
vi.mocked(spawn).mockImplementationOnce((command, args) => {
if (command === 'xclogparser' && args[0] === 'version') {
return createMockProcess('', '', 0);
}
return createMockProcess('', '', 1);
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(true);
expect(result.metadata?.version).toBe(''); // Empty but valid
expect(result.metadata?.path).toBe(''); // Empty but valid
});
test('should handle process spawn errors', async () => {
// Mock spawn throwing an error
vi.mocked(spawn).mockImplementationOnce(() => {
const mockProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn((event, callback) => {
if (event === 'error') {
setTimeout(() => callback(new Error('ENOENT: no such file or directory')), 10);
}
}),
kill: vi.fn()
};
return mockProcess;
});
const result = await EnvironmentValidator['validateXCLogParser']();
expect(result.valid).toBe(false);
expect(result.message).toBe('XCLogParser not found or not executable');
});
});
});
/**
* Helper function to create a mock child process
*/
function createMockProcess(stdout: string, stderr: string, exitCode: number, delay = 10) {
let killed = false;
const mockProcess = {
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn(),
kill: vi.fn(() => {
killed = true;
})
};
// Set up the mock behavior
mockProcess.stdout.on.mockImplementation((event, callback) => {
if (event === 'data' && !killed) {
setTimeout(() => {
if (!killed) callback(Buffer.from(stdout));
}, delay);
}
});
mockProcess.stderr.on.mockImplementation((event, callback) => {
if (event === 'data' && !killed) {
setTimeout(() => {
if (!killed) callback(Buffer.from(stderr));
}, delay);
}
});
mockProcess.on.mockImplementation((event, callback) => {
if (event === 'close' && !killed) {
setTimeout(() => {
if (!killed) callback(exitCode);
}, delay + 5);
}
if (event === 'error' && delay > 5000) {
// Simulate timeout for commands with long delays
setTimeout(() => {
if (!killed) callback(new Error('Command timed out'));
}, 5000);
}
});
return mockProcess;
}