import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from '@jest/globals';
import { AudioInspector } from '../lib/inspector.js';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Audio Inspector Integration Tests', () => {
let inspector;
let testAssetsDir;
let tempTestDir;
beforeAll(async () => {
testAssetsDir = path.join(__dirname, 'assets');
tempTestDir = path.join(__dirname, 'temp-integration');
await fs.mkdir(testAssetsDir, { recursive: true });
await fs.mkdir(tempTestDir, { recursive: true });
});
afterAll(async () => {
await fs.rmdir(tempTestDir, { recursive: true }).catch(() => {});
});
beforeEach(() => {
inspector = new AudioInspector();
});
afterEach(async () => {
// Clean up any test files created during tests
const files = await fs.readdir(tempTestDir).catch(() => []);
for (const file of files) {
await fs.unlink(path.join(tempTestDir, file)).catch(() => {});
}
});
describe('End-to-End File Analysis Workflow', () => {
test('should complete full analysis workflow for various audio formats', async () => {
const testFiles = [
{ name: 'test-mp3.mp3', content: 'ID3\x03\x00\x00\x00', format: 'MP3' },
{ name: 'test-wav.wav', content: 'RIFF\x24\x08\x00\x00WAVE', format: 'WAV' },
{ name: 'test-flac.flac', content: 'fLaC\x00\x00\x00\x22', format: 'FLAC' },
{ name: 'test-ogg.ogg', content: 'OggS\x00\x02\x00\x00', format: 'OGG' }
];
const results = [];
for (const testFile of testFiles) {
const filePath = path.join(tempTestDir, testFile.name);
await fs.writeFile(filePath, testFile.content);
const result = await inspector.analyzeFile(filePath);
results.push(result);
// Verify result structure
expect(result).toHaveProperty('file');
expect(result).toHaveProperty('format');
expect(result).toHaveProperty('tags');
expect(result.file.name).toBe(testFile.name);
expect(result.file.path).toContain(testFile.name);
}
expect(results.length).toBe(testFiles.length);
});
test('should handle mixed file types in batch analysis', async () => {
// Create a mix of audio files and non-audio files
const files = [
{ name: 'audio1.mp3', content: 'fake mp3', isAudio: true },
{ name: 'audio2.wav', content: 'fake wav', isAudio: true },
{ name: 'document.txt', content: 'text content', isAudio: false },
{ name: 'image.jpg', content: 'fake jpg', isAudio: false },
{ name: 'audio3.flac', content: 'fake flac', isAudio: true }
];
for (const file of files) {
await fs.writeFile(path.join(tempTestDir, file.name), file.content);
}
const batchResult = await inspector.analyzeBatch(tempTestDir);
// Should only process audio files
const expectedAudioFiles = files.filter(f => f.isAudio).length;
expect(batchResult.summary.totalFiles).toBe(expectedAudioFiles);
expect(batchResult.results.length).toBe(expectedAudioFiles);
// Verify all results are for audio files
batchResult.results.forEach(result => {
const extension = path.extname(result.file.name);
expect(inspector.supportedFormats).toContain(extension);
});
});
test('should maintain consistency across different analysis methods', async () => {
const testFile = path.join(tempTestDir, 'consistency-test.mp3');
await fs.writeFile(testFile, 'fake mp3 content for consistency test');
// Analyze same file using different methods
const singleResult = await inspector.analyzeFile(testFile);
const batchResult = await inspector.analyzeBatch(tempTestDir);
// Find the same file in batch results
const batchFileResult = batchResult.results.find(
r => r.file.name === 'consistency-test.mp3'
);
expect(batchFileResult).toBeDefined();
// Compare key properties (accounting for potential timing differences)
expect(singleResult.file.name).toBe(batchFileResult.file.name);
expect(singleResult.file.size).toBe(batchFileResult.file.size);
expect(singleResult.format.container).toBe(batchFileResult.format.container);
});
});
describe('Error Recovery and Resilience', () => {
test('should recover from individual file failures in batch processing', async () => {
const files = [
{ name: 'good1.mp3', content: 'fake mp3 content' },
{ name: 'corrupted.mp3', content: '\x00\x00\x00\x00' }, // Corrupted
{ name: 'good2.wav', content: 'fake wav content' },
{ name: 'empty.flac', content: '' }, // Empty file
{ name: 'good3.ogg', content: 'fake ogg content' }
];
for (const file of files) {
await fs.writeFile(path.join(tempTestDir, file.name), file.content);
}
const batchResult = await inspector.analyzeBatch(tempTestDir);
// Should process all files, some may have errors
expect(batchResult.summary.totalFiles).toBe(files.length);
expect(batchResult.results.length).toBe(files.length);
expect(batchResult.summary.successful + batchResult.summary.failed).toBe(files.length);
// Should have some successful results
expect(batchResult.summary.successful).toBeGreaterThan(0);
});
test('should handle permission errors gracefully', async () => {
const testFile = path.join(tempTestDir, 'permission-test.mp3');
await fs.writeFile(testFile, 'test content');
// Try to simulate permission error (OS-dependent)
try {
await fs.chmod(testFile, 0o000); // Remove all permissions
const result = await inspector.analyzeFile(testFile);
// Should handle permission error gracefully
if (result.error) {
expect(result.message).toBeDefined();
}
} finally {
// Restore permissions for cleanup
await fs.chmod(testFile, 0o644).catch(() => {});
}
});
test('should handle extremely large file paths', async () => {
// Create a deeply nested directory structure
let deepPath = tempTestDir;
const maxDepth = 10; // Reasonable depth for testing
for (let i = 0; i < maxDepth; i++) {
deepPath = path.join(deepPath, `level${i}`);
await fs.mkdir(deepPath, { recursive: true });
}
const deepFile = path.join(deepPath, 'deep-file.mp3');
await fs.writeFile(deepFile, 'deep file content');
const result = await inspector.analyzeFile(deepFile);
// Should handle deep paths correctly
expect(result.file.path).toContain('deep-file.mp3');
});
});
describe('Game Development Workflow Integration', () => {
test('should provide comprehensive game audio analysis', async () => {
const gameAudioFiles = [
{
name: 'ui-click.wav',
content: 'short ui sound',
expectedProperties: ['suitableForLoop', 'recommendedCompressionFormat']
},
{
name: 'background-music.mp3',
content: 'longer background track',
expectedProperties: ['platformOptimizations', 'gameDevNotes']
},
{
name: 'voice-dialogue.wav',
content: 'voice recording',
expectedProperties: ['estimatedMemoryUsage', 'compressionRatio']
}
];
for (const audioFile of gameAudioFiles) {
const filePath = path.join(tempTestDir, audioFile.name);
await fs.writeFile(filePath, audioFile.content);
const result = await inspector.analyzeFile(filePath, true); // includeGameAnalysis = true
expect(result).toHaveProperty('gameAudio');
// Check for expected game-specific properties
audioFile.expectedProperties.forEach(prop => {
expect(result.gameAudio).toHaveProperty(prop);
});
// Verify game analysis completeness
expect(result.gameAudio).toHaveProperty('suitableForLoop');
expect(result.gameAudio).toHaveProperty('recommendedCompressionFormat');
expect(result.gameAudio).toHaveProperty('platformOptimizations');
expect(typeof result.gameAudio.estimatedMemoryUsage).toBe('number');
}
});
test('should optimize recommendations for different game contexts', async () => {
const contextFiles = [
{ name: 'sfx-short.wav', expectedLoop: true },
{ name: 'music-long.mp3', expectedLoop: false },
{ name: 'voice-medium.wav', expectedLoop: false }
];
for (const contextFile of contextFiles) {
const filePath = path.join(tempTestDir, contextFile.name);
await fs.writeFile(filePath, 'context test content');
const result = await inspector.analyzeFile(filePath, true);
// Verify context-appropriate recommendations
expect(result.gameAudio).toHaveProperty('platformOptimizations');
expect(result.gameAudio.platformOptimizations).toHaveProperty('mobile');
expect(result.gameAudio.platformOptimizations).toHaveProperty('desktop');
expect(result.gameAudio.platformOptimizations).toHaveProperty('console');
}
});
});
describe('Performance Under Load', () => {
test('should handle large batch operations efficiently', async () => {
const fileCount = 20; // Reasonable number for CI/CD
const files = [];
// Create multiple test files
for (let i = 0; i < fileCount; i++) {
const fileName = `perf-test-${i}.mp3`;
const filePath = path.join(tempTestDir, fileName);
await fs.writeFile(filePath, `fake mp3 content ${i}`);
files.push(fileName);
}
const startTime = Date.now();
const batchResult = await inspector.analyzeBatch(tempTestDir);
const endTime = Date.now();
const processingTime = endTime - startTime;
const avgTimePerFile = processingTime / fileCount;
expect(batchResult.summary.totalFiles).toBe(fileCount);
expect(avgTimePerFile).toBeLessThan(1000); // Should process each file in under 1 second on average
// Verify all files were processed
expect(batchResult.results.length).toBe(fileCount);
});
test('should handle concurrent file analysis requests', async () => {
const concurrentFiles = Array.from({ length: 5 }, (_, i) => ({
name: `concurrent-${i}.mp3`,
path: path.join(tempTestDir, `concurrent-${i}.mp3`)
}));
// Create test files
for (const file of concurrentFiles) {
await fs.writeFile(file.path, `concurrent test content ${file.name}`);
}
// Analyze files concurrently
const startTime = Date.now();
const promises = concurrentFiles.map(file => inspector.analyzeFile(file.path));
const results = await Promise.all(promises);
const endTime = Date.now();
expect(results.length).toBe(concurrentFiles.length);
// All results should be valid
results.forEach((result, index) => {
expect(result.file.name).toBe(concurrentFiles[index].name);
expect(result).toHaveProperty('format');
expect(result).toHaveProperty('tags');
});
// Concurrent processing should be reasonably fast
const totalTime = endTime - startTime;
expect(totalTime).toBeLessThan(10000); // Should complete within 10 seconds
});
});
describe('Data Consistency and Validation', () => {
test('should maintain data consistency across multiple runs', async () => {
const testFile = path.join(tempTestDir, 'consistency-check.mp3');
await fs.writeFile(testFile, 'consistent test content');
// Analyze the same file multiple times
const runs = 3;
const results = [];
for (let i = 0; i < runs; i++) {
const result = await inspector.analyzeFile(testFile);
results.push(result);
}
// Compare results for consistency
const firstResult = results[0];
for (let i = 1; i < runs; i++) {
const currentResult = results[i];
// File properties should be identical
expect(currentResult.file.name).toBe(firstResult.file.name);
expect(currentResult.file.size).toBe(firstResult.file.size);
// Format detection should be consistent
expect(currentResult.format.container).toBe(firstResult.format.container);
expect(currentResult.format.codec).toBe(firstResult.format.codec);
}
});
test('should validate output schema compliance', async () => {
const testFile = path.join(tempTestDir, 'schema-test.mp3');
await fs.writeFile(testFile, 'schema validation content');
const result = await inspector.analyzeFile(testFile);
// Validate required top-level properties
expect(result).toHaveProperty('file');
expect(result).toHaveProperty('format');
expect(result).toHaveProperty('tags');
expect(result).toHaveProperty('source');
// Validate file object structure
expect(result.file).toHaveProperty('path');
expect(result.file).toHaveProperty('name');
expect(result.file).toHaveProperty('size');
// Validate format object structure
expect(result.format).toHaveProperty('container');
expect(result.format).toHaveProperty('codec');
expect(result.format).toHaveProperty('duration');
expect(result.format).toHaveProperty('bitrate');
// Validate tags object structure
expect(result.tags).toHaveProperty('title');
expect(result.tags).toHaveProperty('artist');
expect(result.tags).toHaveProperty('album');
// Validate data types
expect(typeof result.file.size).toBe('number');
expect(typeof result.format.duration).toBe('number');
expect(typeof result.format.bitrate).toBe('number');
});
});
describe('Fallback Mechanisms', () => {
test('should fallback to FFprobe when music-metadata fails', async () => {
const problematicFile = path.join(tempTestDir, 'problematic.unknown');
await fs.writeFile(problematicFile, 'unknown format content');
const result = await inspector.analyzeFile(problematicFile);
// Should handle unknown format gracefully
expect(result).toBeDefined();
if (!result.error) {
// If analysis succeeded, it should indicate fallback usage
expect(result).toHaveProperty('source');
} else {
// If analysis failed, should have error information
expect(result).toHaveProperty('error', true);
expect(result).toHaveProperty('message');
}
});
test('should maintain functionality when external dependencies are unavailable', async () => {
// Test graceful degradation when FFprobe is not available
const testFile = path.join(tempTestDir, 'dependency-test.mp3');
await fs.writeFile(testFile, 'dependency test content');
// The inspector should still function even if FFprobe fails
const result = await inspector.analyzeFile(testFile);
expect(result).toBeDefined();
expect(result.file).toHaveProperty('name');
});
});
});