import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { AudioInspector } from '../lib/inspector.js';
import { promises as fs } from 'fs';
import path from 'path';
describe('AudioInspector', () => {
let inspector;
let testFilesDir;
beforeEach(() => {
inspector = new AudioInspector();
testFilesDir = path.join(process.cwd(), 'tests', 'assets');
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Constructor and Initialization', () => {
test('should initialize with supported formats', () => {
expect(inspector).toBeInstanceOf(AudioInspector);
expect(inspector.supportedFormats).toEqual(
expect.arrayContaining(['.mp3', '.wav', '.flac', '.ogg', '.m4a'])
);
});
test('should have all expected methods', () => {
expect(typeof inspector.analyzeFile).toBe('function');
expect(typeof inspector.analyzeBatch).toBe('function');
expect(typeof inspector.findAudioFiles).toBe('function');
expect(typeof inspector.extractFormatInfo).toBe('function');
expect(typeof inspector.extractTags).toBe('function');
expect(typeof inspector.analyzeForGameAudio).toBe('function');
expect(typeof inspector.getSupportedFormats).toBe('function');
});
});
describe('getSupportedFormats', () => {
test('should return supported formats information', () => {
const formats = inspector.getSupportedFormats();
expect(formats).toHaveProperty('primary');
expect(formats).toHaveProperty('fallback');
expect(formats).toHaveProperty('note');
expect(Array.isArray(formats.primary)).toBe(true);
expect(formats.primary.length).toBeGreaterThan(0);
});
});
describe('extractFormatInfo', () => {
test('should extract format info from valid metadata', () => {
const mockMetadata = {
format: {
container: 'MP3',
codec: 'mp3',
lossless: false,
duration: 180.5,
bitrate: 128000,
sampleRate: 44100,
numberOfChannels: 2,
bitsPerSample: 16
}
};
const formatInfo = inspector.extractFormatInfo(mockMetadata);
expect(formatInfo).toEqual({
container: 'MP3',
codec: 'mp3',
lossless: false,
duration: 180.5,
bitrate: 128000,
sampleRate: 44100,
channels: 2,
bitsPerSample: 16
});
});
test('should handle missing format data gracefully', () => {
const mockMetadata = {};
const formatInfo = inspector.extractFormatInfo(mockMetadata);
expect(formatInfo).toEqual({
container: 'unknown',
codec: 'unknown',
lossless: false,
duration: 0,
bitrate: 0,
sampleRate: 0,
channels: 0,
bitsPerSample: 0
});
});
test('should use codec fallback for container identification', () => {
const mockMetadata = {
format: {
codec: 'flac',
duration: 120
}
};
const formatInfo = inspector.extractFormatInfo(mockMetadata);
expect(formatInfo.container).toBe('FLAC');
});
});
describe('extractTags', () => {
test('should extract tags from valid metadata', () => {
const mockMetadata = {
common: {
title: 'Test Song',
artist: 'Test Artist',
album: 'Test Album',
year: 2024,
genre: ['Electronic', 'Ambient'],
track: { no: 5 },
comment: ['Test comment', 'Another comment']
}
};
const tags = inspector.extractTags(mockMetadata);
expect(tags).toEqual({
title: 'Test Song',
artist: 'Test Artist',
album: 'Test Album',
year: 2024,
genre: 'Electronic, Ambient',
track: 5,
comment: 'Test comment Another comment'
});
});
test('should handle missing tags gracefully', () => {
const mockMetadata = {};
const tags = inspector.extractTags(mockMetadata);
expect(tags).toEqual({
title: '',
artist: '',
album: '',
year: null,
genre: '',
track: null,
comment: ''
});
});
test('should handle single genre string', () => {
const mockMetadata = {
common: {
genre: 'Rock'
}
};
const tags = inspector.extractTags(mockMetadata);
expect(tags.genre).toBe('Rock');
});
});
describe('analyzeForGameAudio', () => {
test('should provide complete game audio analysis', async () => {
const mockBasicInfo = {
format: {
duration: 15.5,
sampleRate: 44100,
channels: 2,
bitsPerSample: 16,
lossless: false
},
file: {
size: 1024000
}
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'test.mp3');
expect(gameAnalysis).toHaveProperty('suitableForLoop');
expect(gameAnalysis).toHaveProperty('recommendedCompressionFormat');
expect(gameAnalysis).toHaveProperty('estimatedMemoryUsage');
expect(gameAnalysis).toHaveProperty('platformOptimizations');
expect(gameAnalysis).toHaveProperty('compressionRatio');
expect(gameAnalysis).toHaveProperty('gameDevNotes');
expect(typeof gameAnalysis.suitableForLoop).toBe('boolean');
expect(typeof gameAnalysis.recommendedCompressionFormat).toBe('string');
expect(typeof gameAnalysis.estimatedMemoryUsage).toBe('number');
expect(typeof gameAnalysis.platformOptimizations).toBe('object');
expect(typeof gameAnalysis.compressionRatio).toBe('number');
});
test('should identify short files as suitable for looping', async () => {
const mockBasicInfo = {
format: { duration: 5.0 },
file: { size: 100000 }
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'short.wav');
expect(gameAnalysis.suitableForLoop).toBe(true);
});
test('should identify long files as not suitable for looping', async () => {
const mockBasicInfo = {
format: { duration: 120.0 },
file: { size: 5000000 }
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'long.mp3');
expect(gameAnalysis.suitableForLoop).toBe(false);
});
test('should recommend appropriate compression formats', async () => {
// High quality lossless file
const losslessInfo = {
format: { lossless: true, bitsPerSample: 24 },
file: { size: 10000000 }
};
let analysis = await inspector.analyzeForGameAudio(losslessInfo, 'test.flac');
expect(analysis.recommendedCompressionFormat).toContain('High Quality');
// Short sound effect
const shortInfo = {
format: { duration: 3.0, lossless: false },
file: { size: 150000 }
};
analysis = await inspector.analyzeForGameAudio(shortInfo, 'sfx.wav');
expect(analysis.recommendedCompressionFormat).toContain('Uncompressed');
// Standard audio
const standardInfo = {
format: { duration: 60.0, lossless: false, bitsPerSample: 16 },
file: { size: 2000000 }
};
analysis = await inspector.analyzeForGameAudio(standardInfo, 'music.mp3');
expect(analysis.recommendedCompressionFormat).toContain('Standard');
});
});
describe('File Analysis - Error Handling', () => {
test('should handle non-existent files gracefully', async () => {
const result = await inspector.analyzeFile('non-existent-file.mp3');
expect(result).toHaveProperty('error', true);
expect(result).toHaveProperty('message');
expect(result).toHaveProperty('file');
expect(result.file).toHaveProperty('error');
});
test('should handle directory instead of file', async () => {
// Create a temporary directory
const tempDir = path.join(testFilesDir, 'temp-dir');
await fs.mkdir(tempDir, { recursive: true });
try {
const result = await inspector.analyzeFile(tempDir);
expect(result).toHaveProperty('error', true);
expect(result.message).toContain('not a file');
} finally {
await fs.rmdir(tempDir);
}
});
test('should handle corrupted file metadata', async () => {
// Create a fake audio file with invalid content
const fakeFile = path.join(testFilesDir, 'fake.mp3');
await fs.writeFile(fakeFile, 'This is not an audio file');
try {
const result = await inspector.analyzeFile(fakeFile);
// Should either return error or fallback to FFprobe
expect(result).toBeDefined();
if (result.error) {
expect(result).toHaveProperty('message');
} else {
expect(result).toHaveProperty('source');
}
} finally {
await fs.unlink(fakeFile).catch(() => {}); // Ignore cleanup errors
}
});
});
describe('Batch Analysis', () => {
let tempDir;
beforeEach(async () => {
tempDir = path.join(testFilesDir, 'batch-test');
await fs.mkdir(tempDir, { recursive: true });
});
afterEach(async () => {
await fs.rmdir(tempDir, { recursive: true }).catch(() => {});
});
test('should handle empty directory', async () => {
const result = await inspector.analyzeBatch(tempDir);
expect(result).toHaveProperty('summary');
expect(result).toHaveProperty('results');
expect(result).toHaveProperty('timestamp');
expect(result.summary.totalFiles).toBe(0);
expect(result.summary.successful).toBe(0);
expect(result.summary.failed).toBe(0);
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBe(0);
});
test('should find and analyze audio files', async () => {
// Create fake audio files
const mp3File = path.join(tempDir, 'test.mp3');
const wavFile = path.join(tempDir, 'test.wav');
const txtFile = path.join(tempDir, 'readme.txt');
await fs.writeFile(mp3File, 'fake mp3 content');
await fs.writeFile(wavFile, 'fake wav content');
await fs.writeFile(txtFile, 'text file content');
const result = await inspector.analyzeBatch(tempDir);
expect(result.summary.totalFiles).toBe(2); // Only audio files
expect(result.results.length).toBe(2);
// Clean up
await fs.unlink(mp3File).catch(() => {});
await fs.unlink(wavFile).catch(() => {});
await fs.unlink(txtFile).catch(() => {});
});
test('should handle recursive directory scanning', async () => {
const subDir = path.join(tempDir, 'subdirectory');
await fs.mkdir(subDir, { recursive: true });
const mp3File = path.join(subDir, 'nested.mp3');
await fs.writeFile(mp3File, 'fake mp3 content');
const result = await inspector.analyzeBatch(tempDir, true); // recursive = true
expect(result.summary.totalFiles).toBe(1);
const nonRecursiveResult = await inspector.analyzeBatch(tempDir, false);
expect(nonRecursiveResult.summary.totalFiles).toBe(0);
// Clean up
await fs.unlink(mp3File).catch(() => {});
await fs.rmdir(subDir).catch(() => {});
});
});
describe('Helper Methods', () => {
test('getContainerFromCodec should map codecs correctly', () => {
expect(inspector.getContainerFromCodec('mp3')).toBe('MP3');
expect(inspector.getContainerFromCodec('wav')).toBe('WAV');
expect(inspector.getContainerFromCodec('flac')).toBe('FLAC');
expect(inspector.getContainerFromCodec('vorbis')).toBe('OGG');
expect(inspector.getContainerFromCodec('aac')).toBe('M4A');
expect(inspector.getContainerFromCodec('opus')).toBe('OPUS');
expect(inspector.getContainerFromCodec('unknown')).toBe('unknown');
expect(inspector.getContainerFromCodec(null)).toBe('unknown');
});
test('checkLoopSuitability should work correctly', () => {
expect(inspector.checkLoopSuitability({ duration: 15 })).toBe(true);
expect(inspector.checkLoopSuitability({ duration: 45 })).toBe(false);
expect(inspector.checkLoopSuitability({})).toBe(false);
});
test('recommendCompressionFormat should provide appropriate recommendations', () => {
expect(inspector.recommendCompressionFormat({ lossless: true }, 1000000))
.toContain('High Quality');
expect(inspector.recommendCompressionFormat({ bitsPerSample: 24 }, 1000000))
.toContain('High Quality');
expect(inspector.recommendCompressionFormat({ duration: 5 }, 500000))
.toContain('Uncompressed');
expect(inspector.recommendCompressionFormat({ duration: 60, lossless: false }, 2000000))
.toContain('Standard');
});
test('getPlatformOptimizations should provide platform-specific advice', () => {
const optimizations = inspector.getPlatformOptimizations({
sampleRate: 48000,
channels: 2
});
expect(optimizations).toHaveProperty('mobile');
expect(optimizations).toHaveProperty('desktop');
expect(optimizations).toHaveProperty('console');
expect(optimizations.mobile).toContain('downsampling');
expect(optimizations.desktop).toBe('Use original quality');
expect(optimizations.console).toBe('Optimized');
});
test('getGameDevNotes should provide relevant development notes', () => {
const notes = inspector.getGameDevNotes(
{ sampleRate: 96000, bitsPerSample: 24 },
15 * 1024 * 1024 // 15MB
);
expect(notes).toContain('sample rate');
expect(notes).toContain('bit depth');
expect(notes).toContain('compression');
});
});
describe('FFprobe Integration', () => {
test('analyzeWithFFprobe should handle spawn errors', async () => {
// Mock spawn to simulate ffprobe not found
const originalSpawn = await import('child_process').then(m => m.spawn);
// This test verifies error handling rather than actual FFprobe functionality
// since FFprobe might not be available in test environment
const mockMetadata = {
format: {
container: 'test',
codec: 'test',
duration: 0,
bitrate: 0,
sampleRate: 0,
numberOfChannels: 0,
bitsPerSample: 0
},
common: {}
};
// Test that the method exists and returns expected structure
expect(typeof inspector.analyzeWithFFprobe).toBe('function');
});
});
describe('Edge Cases and Stress Tests', () => {
test('should handle files with unusual sample rates', async () => {
const mockBasicInfo = {
format: {
sampleRate: 8000, // Very low sample rate
channels: 1,
duration: 10
},
file: { size: 80000 }
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'low-quality.wav');
expect(gameAnalysis).toBeDefined();
expect(gameAnalysis.platformOptimizations.mobile).toBe('Optimized');
});
test('should handle files with many channels', async () => {
const mockBasicInfo = {
format: {
sampleRate: 44100,
channels: 8, // Surround sound
duration: 60
},
file: { size: 10000000 }
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'surround.wav');
expect(gameAnalysis.platformOptimizations.console).toContain('downmix');
});
test('should handle very short audio files', async () => {
const mockBasicInfo = {
format: {
duration: 0.1, // 100ms
sampleRate: 44100,
channels: 2
},
file: { size: 8820 }
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'very-short.wav');
expect(gameAnalysis.suitableForLoop).toBe(true);
expect(gameAnalysis.estimatedMemoryUsage).toBeGreaterThan(0);
});
test('should handle very large audio files', async () => {
const mockBasicInfo = {
format: {
duration: 3600, // 1 hour
sampleRate: 96000,
channels: 2,
bitsPerSample: 24
},
file: { size: 500 * 1024 * 1024 } // 500MB
};
const gameAnalysis = await inspector.analyzeForGameAudio(mockBasicInfo, 'large-file.wav');
expect(gameAnalysis.gameDevNotes).toContain('compression');
expect(gameAnalysis.estimatedMemoryUsage).toBeGreaterThan(1000000);
});
});
describe('Input Validation', () => {
test('should validate file paths', async () => {
const invalidPaths = ['', null, undefined, 123, {}];
for (const invalidPath of invalidPaths) {
const result = await inspector.analyzeFile(invalidPath);
expect(result).toHaveProperty('error', true);
}
});
test('should handle malformed metadata objects', () => {
const malformedInputs = [null, undefined, 'string', 123, []];
for (const input of malformedInputs) {
expect(() => inspector.extractFormatInfo(input)).not.toThrow();
expect(() => inspector.extractTags(input)).not.toThrow();
}
});
});
});