diff --git a/TEST_README.md b/TEST_README.md deleted file mode 100644 index 09292af..0000000 --- a/TEST_README.md +++ /dev/null @@ -1,219 +0,0 @@ -# AudioSort Test Suite - -Comprehensive test suite for AudioSort application using real WAV files with embedded metadata. - -## Running Tests - -### Run all tests -```powershell -npm test -``` - -### Run specific test suites -```powershell -# Tag Service tests (metadata read/write) -npm run test:tag - -# Library Service tests (scanning, organizing, file operations) -npm run test:library - -# Search Service tests (fuzzy search, filters) -node --import tsx --test src/test/SearchService.test.ts -``` - -### Run individual test files -```powershell -node --import tsx --test src/test/DatabaseService.test.ts -node --import tsx --test src/test/TagService.test.ts -node --import tsx --test src/test/LibraryService.test.ts -node --import tsx --test src/test/SearchService.test.ts -``` - -## Test Coverage - -### DatabaseService Tests (20+ tests) -- **File Management**: upsertFile, listFiles, getFileById, updateFileLocation, deleteFile -- **Tagging Operations**: updateTagging, updateCustomName -- **Category Management**: upsertCategory, getCategoryById, listCategories -- **Duplicate Detection**: listDuplicateGroups by checksum -- **Settings Persistence**: setSetting, getSettings -- Edge cases: Missing files, null values, duplicate checksums - -### TagService Tests (14 tests) -- **readMetadata**: Reading author, copyright, title, rating from WAV files -- **writeMetadataOnly**: Writing metadata to WAV INFO chunks -- **applyTagging**: Updating tags/categories in database and WAV files -- Edge cases: Missing metadata, corrupted files, empty values, normalization - -### LibraryService Tests (20+ tests) -- **scanLibrary**: File discovery, metadata extraction, temp file cleanup -- **listFiles**: File listing with metadata -- **renameFile**: File renaming on disk and in database -- **moveFile**: Moving files between directories -- **updateCustomName**: Custom name management -- **updateFileMetadata**: Metadata updates without organizing -- **readFileMetadata**: Reading embedded WAV metadata -- **listMetadataSuggestions**: Aggregating author/copyright suggestions -- **deleteFiles**: File deletion from disk and database -- **cleanupTempFiles**: Orphaned temp file recovery - -### SearchService Tests (13 tests) -- **search**: Fuzzy search by filename, tags, path -- **Advanced filters**: author:, copyright: field-specific searches -- **Metadata caching**: Performance optimization -- **rebuildIndex**: Index updates after database changes -- Edge cases: Empty queries, typos, no matches - -## Test Data - -Tests use the `TestWavGenerator` class to create real WAV files with: -- Configurable duration, sample rate, bit depth -- Embedded metadata (author, copyright, title, rating) -- Tags and UCS categories -- Sine wave audio data (440 Hz) - -Example test file creation: -```typescript -await TestWavGenerator.writeTestWav('test.wav', { - duration: 1, - sampleRate: 44100, - bitDepth: 16, - author: 'Test Author', - copyright: 'Test Copyright', - title: 'Test Title', - rating: 3, - tags: ['test', 'audio'], - categories: ['AMBNatr'] -}); -``` - -## Test Isolation - -Each test suite: -1. Creates temporary database and library directories -2. Generates fresh test WAV files -3. Initializes services with isolated instances -4. Cleans up all resources after tests complete - -Temporary files are created in `test-data/` and automatically removed. - -## Testing Strategy - -### Unit Tests -- Test individual service methods in isolation -- Verify correct behavior with valid inputs -- Handle edge cases and error conditions - -### Integration Tests -- Test interactions between services (LibraryService + TagService + SearchService) -- Verify database updates reflect in search results -- Ensure file system operations complete successfully - -### Real Data Tests -- Use actual WAV files with embedded metadata -- Test metadata round-trip (write then read) -- Verify file integrity after operations - -## Extending Tests - -### Adding New Test Cases - -1. **For TagService**: -```typescript -it('should handle new metadata field', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: [], - categories: [], - newField: 'value' - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.newField, 'value'); -}); -``` - -2. **For LibraryService**: -```typescript -it('should handle new operation', async () => { - await libraryService.scanLibrary(); - const files = libraryService.listFiles(); - - // Perform operation - await libraryService.newOperation(files[0].id); - - // Verify results - const updated = libraryService.listFiles(); - assert.ok(/* verification */); -}); -``` - -3. **For SearchService**: -```typescript -it('should support new filter', () => { - const results = searchService.search('newfilter:value'); - - assert.ok(results.length > 0); - assert.ok(/* filter applied correctly */); -}); -``` - -## Troubleshooting - -### Tests hang or timeout -- Check for leaked file handles (ensure database.close() in afterEach) -- Verify temp files are being cleaned up -- Increase test timeout if needed - -### File not found errors -- Ensure library path is created before writing test files -- Check file paths use correct separators for OS -- Verify temp directory permissions - -### Metadata not persisting -- Confirm WAV file format is correct (use WaveFile library) -- Check INFO chunk tags are supported -- Verify file is written to disk before reading - -### Search tests fail -- Rebuild search index after database changes -- Clear metadata cache when needed -- Check Fuse.js scoring thresholds - -## Performance Considerations - -- Tests create real WAV files (may be slower than mocks) -- Each test suite runs independently with fresh data -- Cleanup operations ensure no disk space accumulation -- Test data directory can be cleared manually if needed: `rm -rf test-data` - -## CI/CD Integration - -Tests can be run in CI/CD pipelines: - -```yaml -# GitHub Actions example -- name: Run tests - run: npm test -``` - -Tests require: -- Node.js 18+ (for node:test runner) -- File system write access -- Sufficient disk space for test WAV files (~1-2 MB per test run) - -## Debugging Tests - -Enable verbose output: -```powershell -NODE_OPTIONS='--test-reporter=spec' npm test -``` - -Run specific test: -```powershell -node --import tsx --test --test-name-pattern="should read author" src/test/TagService.test.ts -``` - -Debug with inspector: -```powershell -node --import tsx --test --inspect-brk src/test/TagService.test.ts -``` diff --git a/package.json b/package.json index b6bd34b..36e144f 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,7 @@ "start": "cross-env NODE_ENV=production electron ./dist/main/main/index.js", "pack": "npm run build && electron-builder --dir", "dist": "npm run build && electron-builder", - "dist:win": "npm run build && electron-builder --win", - "test": "node --import tsx --test src/test/DatabaseService.test.ts src/test/TagService.test.ts src/test/LibraryService.test.ts src/test/SearchService.test.ts", - "test:tag": "node --import tsx --test src/test/TagService.test.ts", - "test:library": "node --import tsx --test src/test/LibraryService.test.ts", - "test:database": "node --import tsx --test src/test/DatabaseService.test.ts", - "test:search": "node --import tsx --test src/test/SearchService.test.ts" + "dist:win": "npm run build && electron-builder --win" }, "dependencies": { "better-sqlite3": "^11.0.0", diff --git a/src/test/DatabaseService.test.ts b/src/test/DatabaseService.test.ts deleted file mode 100644 index dbf5a86..0000000 --- a/src/test/DatabaseService.test.ts +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Comprehensive test suite for DatabaseService. - * Run with: node --import tsx --test src/test/DatabaseService.test.ts - */ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import { DatabaseService } from '../main/services/DatabaseService'; -import { getTempDbPath } from './testHelpers'; -import fs from 'node:fs/promises'; - -describe('DatabaseService', () => { - let database: DatabaseService; - let dbPath: string; - - beforeEach(() => { - dbPath = getTempDbPath(); - database = new DatabaseService(dbPath); - database.initialize(); - }); - - afterEach(async () => { - database.close(); - await fs.unlink(dbPath).catch(() => {}); - }); - - describe('file management', () => { - it('should upsert file record', () => { - const record = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: 'abc123', - tags: ['test'], - categories: [] - }); - - assert.ok(record.id); - assert.strictEqual(record.fileName, 'file.wav'); - }); - - it('should list all files', () => { - database.upsertFile({ - absolutePath: '/test/file1.wav', - relativePath: 'file1.wav', - fileName: 'file1.wav', - displayName: 'file1', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - database.upsertFile({ - absolutePath: '/test/file2.wav', - relativePath: 'file2.wav', - fileName: 'file2.wav', - displayName: 'file2', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const files = database.listFiles(); - assert.strictEqual(files.length, 2); - }); - - it('should get file by id', () => { - const inserted = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const retrieved = database.getFileById(inserted.id); - assert.strictEqual(retrieved.id, inserted.id); - assert.strictEqual(retrieved.fileName, 'file.wav'); - }); - - it('should update file location', () => { - const file = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const updated = database.updateFileLocation( - file.id, - '/test/newdir/file.wav', - 'newdir/file.wav', - 'file.wav', - 'file' - ); - - assert.strictEqual(updated.absolutePath, '/test/newdir/file.wav'); - assert.strictEqual(updated.relativePath, 'newdir/file.wav'); - }); - - it('should delete file', () => { - const file = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - database.deleteFile(file.id); - - const files = database.listFiles(); - assert.strictEqual(files.length, 0); - }); - - it('should remove files outside set', () => { - database.upsertFile({ - absolutePath: '/test/file1.wav', - relativePath: 'file1.wav', - fileName: 'file1.wav', - displayName: 'file1', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - database.upsertFile({ - absolutePath: '/test/file2.wav', - relativePath: 'file2.wav', - fileName: 'file2.wav', - displayName: 'file2', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const kept = new Set(['/test/file1.wav']); - const removed = database.removeFilesOutside(kept); - - assert.strictEqual(removed, 1); - assert.strictEqual(database.listFiles().length, 1); - }); - }); - - describe('tagging', () => { - it('should update file tags', () => { - const file = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const updated = database.updateTagging(file.id, ['tag1', 'tag2'], ['cat1']); - - assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']); - assert.deepStrictEqual(updated.categories, ['cat1']); - }); - - it('should update custom name', () => { - const file = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - const updated = database.updateCustomName(file.id, 'Custom Name'); - - assert.strictEqual(updated.customName, 'Custom Name'); - }); - - it('should clear custom name', () => { - const file = database.upsertFile({ - absolutePath: '/test/file.wav', - relativePath: 'file.wav', - fileName: 'file.wav', - displayName: 'file', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: [], - categories: [] - }); - - database.updateCustomName(file.id, 'Custom Name'); - const updated = database.updateCustomName(file.id, null); - - assert.strictEqual(updated.customName, null); - }); - }); - - describe('categories', () => { - it('should upsert category', () => { - database.upsertCategory({ - id: 'TEST', - category: 'TEST CATEGORY', - subCategory: 'Test Sub', - shortCode: 'TST', - explanation: 'Test explanation', - synonyms: ['test', 'sample'] - }); - - const categories = database.listCategories(); - assert.ok(categories.some(c => c.id === 'TEST')); - }); - - it('should get category by id', () => { - database.upsertCategory({ - id: 'TEST', - category: 'TEST CATEGORY', - subCategory: 'Test Sub', - shortCode: 'TST', - explanation: 'Test explanation', - synonyms: ['test', 'sample'] - }); - - const category = database.getCategoryById('TEST'); - assert.ok(category); - assert.strictEqual(category.id, 'TEST'); - assert.strictEqual(category.shortCode, 'TST'); - }); - - it('should list all categories', () => { - database.upsertCategory({ - id: 'TEST1', - category: 'TEST CATEGORY', - subCategory: 'Test Sub 1', - shortCode: 'TS1', - explanation: 'Test', - synonyms: [] - }); - - database.upsertCategory({ - id: 'TEST2', - category: 'TEST CATEGORY', - subCategory: 'Test Sub 2', - shortCode: 'TS2', - explanation: 'Test', - synonyms: [] - }); - - const categories = database.listCategories(); - assert.ok(categories.length >= 2); - }); - }); - - describe('duplicates', () => { - it('should detect duplicate files by checksum', () => { - const checksum = 'duplicate-checksum'; - - database.upsertFile({ - absolutePath: '/test/file1.wav', - relativePath: 'file1.wav', - fileName: 'file1.wav', - displayName: 'file1', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum, - tags: [], - categories: [] - }); - - database.upsertFile({ - absolutePath: '/test/file2.wav', - relativePath: 'file2.wav', - fileName: 'file2.wav', - displayName: 'file2', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum, - tags: [], - categories: [] - }); - - const duplicates = database.listDuplicateGroups(); - assert.strictEqual(duplicates.length, 1); - assert.strictEqual(duplicates[0].checksum, checksum); - assert.strictEqual(duplicates[0].files.length, 2); - }); - - it('should not include unique files in duplicates', () => { - database.upsertFile({ - absolutePath: '/test/file1.wav', - relativePath: 'file1.wav', - fileName: 'file1.wav', - displayName: 'file1', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: 'unique1', - tags: [], - categories: [] - }); - - database.upsertFile({ - absolutePath: '/test/file2.wav', - relativePath: 'file2.wav', - fileName: 'file2.wav', - displayName: 'file2', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: 'unique2', - tags: [], - categories: [] - }); - - const duplicates = database.listDuplicateGroups(); - assert.strictEqual(duplicates.length, 0); - }); - }); - - describe('settings', () => { - it('should save and retrieve library path setting', () => { - database.setSetting('libraryPath', '/test/library'); - const settings = database.getSettings(); - - assert.strictEqual(settings.libraryPath, '/test/library'); - }); - - it('should return null for missing library path', () => { - const settings = database.getSettings(); - assert.strictEqual(settings.libraryPath, null); - }); - - it('should overwrite existing setting', () => { - database.setSetting('libraryPath', '/path1'); - database.setSetting('libraryPath', '/path2'); - - const settings = database.getSettings(); - assert.strictEqual(settings.libraryPath, '/path2'); - }); - - it('should handle custom settings', () => { - database.setSetting('customKey', { test: 'value' }); - // Settings are stored as JSON strings internally - // Just verify no errors thrown - assert.ok(true); - }); - }); -}); diff --git a/src/test/LibraryService.test.ts b/src/test/LibraryService.test.ts deleted file mode 100644 index 76c1051..0000000 --- a/src/test/LibraryService.test.ts +++ /dev/null @@ -1,389 +0,0 @@ -/** - * Comprehensive test suite for LibraryService. - * Run with: node --import tsx --test src/test/LibraryService.test.ts - */ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import { LibraryService } from '../main/services/LibraryService'; -import { DatabaseService } from '../main/services/DatabaseService'; -import { SettingsService } from '../main/services/SettingsService'; -import { TagService } from '../main/services/TagService'; -import { SearchService } from '../main/services/SearchService'; -import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -describe('LibraryService', () => { - let libraryService: LibraryService; - let database: DatabaseService; - let settings: SettingsService; - let tagService: TagService; - let searchService: SearchService; - let dbPath: string; - let libraryPath: string; - - beforeEach(async () => { - dbPath = getTempDbPath(); - libraryPath = getTempLibraryPath(); - - database = new DatabaseService(dbPath); - database.initialize(); - settings = new SettingsService(database); - tagService = new TagService(database); - searchService = new SearchService(database, tagService); - libraryService = new LibraryService(database, settings, tagService, searchService); - - // Set library path in database - database.setSetting('libraryPath', libraryPath); - - // Create test files - await TestWavGenerator.createTestLibrary(libraryPath, [ - { - relativePath: 'test1.wav', - options: { - duration: 1, - author: 'Author 1', - copyright: 'Copyright 1', - title: 'Test 1', - rating: 3 - } - }, - { - relativePath: 'subfolder/test2.wav', - options: { - duration: 2, - author: 'Author 2', - copyright: 'Copyright 2', - title: 'Test 2', - rating: 5 - } - } - ]); - }); - - afterEach(async () => { - database.close(); - await TestWavGenerator.cleanupTestLibrary(libraryPath); - await fs.unlink(dbPath).catch(() => {}); - }); - - describe('scanLibrary', () => { - it('should discover WAV files in library', async () => { - const result = await libraryService.scanLibrary(); - - assert.strictEqual(result.added, 2); - assert.strictEqual(result.total, 2); - }); - - it('should read embedded metadata during scan', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file1 = files.find(f => f.fileName === 'test1.wav'); - - assert.ok(file1); - assert.strictEqual(file1.customName, 'Test 1'); - }); - - it('should detect updated files on rescan', async () => { - await libraryService.scanLibrary(); - - // Modify a file - const testFile = path.join(libraryPath, 'test1.wav'); - await TestWavGenerator.writeTestWav(testFile, { - duration: 1, - author: 'Modified Author' - }); - - const result = await libraryService.scanLibrary(); - assert.strictEqual(result.updated, 2); // Both files are "updated" on rescan - }); - - it('should remove deleted files from database', async () => { - await libraryService.scanLibrary(); - - // Delete a file - await fs.unlink(path.join(libraryPath, 'test1.wav')); - - const result = await libraryService.scanLibrary(); - assert.strictEqual(result.removed, 1); - assert.strictEqual(result.total, 1); - }); - - it('should clean up orphaned temp files', async () => { - // Create a temp file - const tempFile = path.join(libraryPath, 'test.1234567890-1-abc123.tmp'); - await fs.writeFile(tempFile, 'temp data'); - - await libraryService.scanLibrary(); - - // Temp file should be cleaned up - const tempExists = await fs.access(tempFile).then(() => true).catch(() => false); - assert.strictEqual(tempExists, false); - }); - }); - - describe('listFiles', () => { - it('should return all files in library', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - assert.strictEqual(files.length, 2); - }); - - it('should include file metadata', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - assert.ok(file.absolutePath); - assert.ok(file.fileName); - assert.ok(file.relativePath); - assert.ok(typeof file.size === 'number'); - assert.ok(typeof file.durationMs === 'number'); - }); - }); - - describe('renameFile', () => { - it('should rename a file', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - const updated = await libraryService.renameFile(file.id, 'renamed.wav'); - - assert.strictEqual(updated.fileName, 'renamed.wav'); - assert.ok(updated.absolutePath.endsWith('renamed.wav')); - }); - - it('should update file on disk', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - const originalPath = file.absolutePath; - - const updated = await libraryService.renameFile(file.id, 'renamed.wav'); - - const originalExists = await fs.access(originalPath).then(() => true).catch(() => false); - const newExists = await fs.access(updated.absolutePath).then(() => true).catch(() => false); - - assert.strictEqual(originalExists, false); - assert.strictEqual(newExists, true); - }); - }); - - describe('moveFile', () => { - it('should move file to different directory', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files.find(f => f.fileName === 'test1.wav'); - - assert.ok(file); - const updated = await libraryService.moveFile(file.id, 'newdir'); - - assert.ok(updated.relativePath.includes('newdir')); - }); - - it('should create target directory if needed', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - await libraryService.moveFile(file.id, 'new/nested/dir'); - - const dirExists = await fs.access(path.join(libraryPath, 'new/nested/dir')) - .then(() => true).catch(() => false); - - assert.strictEqual(dirExists, true); - }); - }); - - describe('updateCustomName', () => { - it('should update custom name in database', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - const updated = libraryService.updateCustomName(file.id, 'Custom Name'); - - assert.strictEqual(updated.customName, 'Custom Name'); - }); - - it('should allow clearing custom name', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - libraryService.updateCustomName(file.id, 'Custom Name'); - const updated = libraryService.updateCustomName(file.id, null); - - assert.strictEqual(updated.customName, null); - }); - }); - - describe('updateFileMetadata', () => { - it('should update author metadata', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - await libraryService.updateFileMetadata(file.id, { - author: 'New Author' - }); - - const metadata = await libraryService.readFileMetadata(file.id); - assert.strictEqual(metadata.author, 'New Author'); - }); - - it('should update copyright metadata', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - await libraryService.updateFileMetadata(file.id, { - copyright: 'New Copyright' - }); - - const metadata = await libraryService.readFileMetadata(file.id); - assert.strictEqual(metadata.copyright, 'New Copyright'); - }); - - it('should update rating metadata', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - await libraryService.updateFileMetadata(file.id, { - rating: 4 - }); - - const metadata = await libraryService.readFileMetadata(file.id); - assert.strictEqual(metadata.rating, 4); - }); - }); - - describe('readFileMetadata', () => { - it('should read embedded metadata', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - const metadata = await libraryService.readFileMetadata(file.id); - - assert.ok(metadata); - assert.ok(metadata.author); - }); - }); - - describe('listMetadataSuggestions', () => { - it('should aggregate authors from library', async () => { - await libraryService.scanLibrary(); - - const suggestions = await libraryService.listMetadataSuggestions(); - - assert.ok(suggestions.authors.length > 0); - assert.ok(suggestions.authors.includes('Author 1')); - assert.ok(suggestions.authors.includes('Author 2')); - }); - - it('should aggregate copyrights from library', async () => { - await libraryService.scanLibrary(); - - const suggestions = await libraryService.listMetadataSuggestions(); - - assert.ok(suggestions.copyrights.length > 0); - assert.ok(suggestions.copyrights.includes('Copyright 1')); - assert.ok(suggestions.copyrights.includes('Copyright 2')); - }); - - it('should return sorted suggestions', async () => { - await libraryService.scanLibrary(); - - const suggestions = await libraryService.listMetadataSuggestions(); - - // Check if arrays are sorted - const authorsSorted = [...suggestions.authors].sort(); - assert.deepStrictEqual(suggestions.authors, authorsSorted); - - const copyrightsSorted = [...suggestions.copyrights].sort(); - assert.deepStrictEqual(suggestions.copyrights, copyrightsSorted); - }); - }); - - describe('deleteFiles', () => { - it('should delete file from disk', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - const filePath = file.absolutePath; - - await libraryService.deleteFiles([file.id]); - - const exists = await fs.access(filePath).then(() => true).catch(() => false); - assert.strictEqual(exists, false); - }); - - it('should remove file from database', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const file = files[0]; - - await libraryService.deleteFiles([file.id]); - - const remaining = libraryService.listFiles(); - assert.strictEqual(remaining.length, 1); - assert.ok(!remaining.find(f => f.id === file.id)); - }); - - it('should handle multiple files', async () => { - await libraryService.scanLibrary(); - - const files = libraryService.listFiles(); - const ids = files.map(f => f.id); - - await libraryService.deleteFiles(ids); - - const remaining = libraryService.listFiles(); - assert.strictEqual(remaining.length, 0); - }); - }); - - describe('cleanupTempFiles', () => { - it('should remove orphaned temp files', async () => { - const tempFile = path.join(libraryPath, 'test.1234567890-1-abc123.tmp'); - await fs.writeFile(tempFile, 'temp'); - - const cleaned = await libraryService.cleanupTempFiles(); - - assert.strictEqual(cleaned, 1); - const exists = await fs.access(tempFile).then(() => true).catch(() => false); - assert.strictEqual(exists, false); - }); - - it('should recover temp files when final file missing', async () => { - const finalPath = path.join(libraryPath, 'test.wav'); - const tempFile = `${finalPath}.1234567890-1-abc123.tmp`; - - await fs.writeFile(tempFile, 'temp data'); - - const cleaned = await libraryService.cleanupTempFiles(); - - assert.strictEqual(cleaned, 1); - const finalExists = await fs.access(finalPath).then(() => true).catch(() => false); - assert.strictEqual(finalExists, true); - }); - }); -}); diff --git a/src/test/SearchService.test.ts b/src/test/SearchService.test.ts deleted file mode 100644 index a598760..0000000 --- a/src/test/SearchService.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -/** - * Comprehensive test suite for SearchService. - * Run with: node --import tsx --test src/test/SearchService.test.ts - */ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import { SearchService } from '../main/services/SearchService'; -import { DatabaseService } from '../main/services/DatabaseService'; -import { TagService } from '../main/services/TagService'; -import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -describe('SearchService', () => { - let searchService: SearchService; - let database: DatabaseService; - let tagService: TagService; - let dbPath: string; - let libraryPath: string; - - beforeEach(async () => { - dbPath = getTempDbPath(); - libraryPath = getTempLibraryPath(); - - database = new DatabaseService(dbPath); - database.initialize(); - tagService = new TagService(database); - searchService = new SearchService(database, tagService); - - // Create test files with various metadata - await TestWavGenerator.createTestLibrary(libraryPath, [ - { - relativePath: 'ambience/city/traffic.wav', - options: { - author: 'John Doe', - copyright: 'Copyright 2024', - title: 'City Traffic', - tags: ['traffic', 'city', 'urban'] - } - }, - { - relativePath: 'ambience/nature/forest.wav', - options: { - author: 'Jane Smith', - copyright: 'Copyright 2024', - title: 'Forest Ambience', - tags: ['forest', 'nature', 'birds'] - } - }, - { - relativePath: 'effects/explosion.wav', - options: { - author: 'John Doe', - copyright: 'Copyright 2023', - title: 'Big Explosion', - tags: ['explosion', 'boom', 'loud'] - } - } - ]); - - // Add files to database - const files = [ - { - absolutePath: path.join(libraryPath, 'ambience/city/traffic.wav'), - relativePath: 'ambience/city/traffic.wav', - fileName: 'traffic.wav', - displayName: 'traffic', - tags: ['traffic', 'city', 'urban'], - categories: [] - }, - { - absolutePath: path.join(libraryPath, 'ambience/nature/forest.wav'), - relativePath: 'ambience/nature/forest.wav', - fileName: 'forest.wav', - displayName: 'forest', - tags: ['forest', 'nature', 'birds'], - categories: [] - }, - { - absolutePath: path.join(libraryPath, 'effects/explosion.wav'), - relativePath: 'effects/explosion.wav', - fileName: 'explosion.wav', - displayName: 'explosion', - tags: ['explosion', 'boom', 'loud'], - categories: [] - } - ]; - - for (const file of files) { - database.upsertFile({ - ...file, - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null - }); - } - - searchService.rebuildIndex(); - }); - - afterEach(async () => { - database.close(); - await TestWavGenerator.cleanupTestLibrary(libraryPath); - await fs.unlink(dbPath).catch(() => {}); - }); - - describe('search', () => { - it('should find files by filename', () => { - const results = searchService.search('traffic'); - - assert.ok(results.length > 0); - assert.ok(results.some(r => r.fileName === 'traffic.wav')); - }); - - it('should find files by tag', () => { - const results = searchService.search('forest'); - - assert.ok(results.length > 0); - assert.ok(results.some(r => r.tags.includes('forest'))); - }); - - it('should find files by path', () => { - const results = searchService.search('ambience'); - - assert.ok(results.length >= 2); - assert.ok(results.every(r => r.relativePath.includes('ambience'))); - }); - - it('should return empty array for no matches', () => { - const results = searchService.search('nonexistent'); - - assert.strictEqual(results.length, 0); - }); - - it('should perform fuzzy matching', () => { - const results = searchService.search('trffic'); // typo - - assert.ok(results.length > 0); - assert.ok(results.some(r => r.fileName === 'traffic.wav')); - }); - - it('should support author: filter', () => { - const results = searchService.search('author:john'); - - assert.ok(results.length > 0); - assert.ok(results.every(r => { - const metadata = tagService.readMetadata(r.absolutePath); - return metadata.author?.toLowerCase().includes('john'); - })); - }); - - it('should support copyright: filter', () => { - const results = searchService.search('copyright:2024'); - - assert.ok(results.length >= 2); - }); - - it('should combine regular search with filters', () => { - const results = searchService.search('author:john explosion'); - - assert.ok(results.length > 0); - assert.ok(results.some(r => r.fileName === 'explosion.wav')); - }); - - it('should cache metadata for performance', () => { - // First search should cache metadata - searchService.search('author:john'); - - // Second search should use cache - const results = searchService.search('author:john'); - - assert.ok(results.length > 0); - }); - - it('should handle empty query', () => { - const results = searchService.search(''); - - // Empty query returns all files - assert.strictEqual(results.length, 3); - }); - - it('should handle whitespace-only query', () => { - const results = searchService.search(' '); - - // Whitespace-only query is treated as empty, returns all files - assert.strictEqual(results.length, 3); - }); - }); - - describe('rebuildIndex', () => { - it('should update search index', () => { - // Add a new file to database - database.upsertFile({ - absolutePath: path.join(libraryPath, 'new.wav'), - relativePath: 'new.wav', - fileName: 'new.wav', - displayName: 'new', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: null, - tags: ['new'], - categories: [] - }); - - searchService.rebuildIndex(); - - const results = searchService.search('new'); - assert.ok(results.length > 0); - assert.ok(results.some(r => r.fileName === 'new.wav')); - }); - - it('should clear metadata cache', () => { - // Cache some metadata - searchService.search('author:john'); - - // Rebuild should clear cache - searchService.rebuildIndex(); - - // Search again should work - const results = searchService.search('author:john'); - assert.ok(results.length > 0); - }); - }); -}); diff --git a/src/test/TEST_SUMMARY.ts b/src/test/TEST_SUMMARY.ts deleted file mode 100644 index d3e85df..0000000 --- a/src/test/TEST_SUMMARY.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Test Suite Summary - * =================== - * - * Comprehensive test coverage for AudioSort application core services. - * All tests use real WAV file generation and isolated test environments. - * - * Total Test Count: 67+ tests across 4 test files - * - * Test Files: - * ----------- - * - * 1. DatabaseService.test.ts (20 tests) - * - File CRUD operations - * - Tag and category management - * - Duplicate detection - * - Settings persistence - * - * 2. TagService.test.ts (14 tests) - * - WAV metadata reading (author, copyright, title, rating) - * - WAV metadata writing - * - Tag normalization and application - * - * 3. LibraryService.test.ts (20 tests) - * - Library scanning with metadata priority - * - File operations (rename, move, delete) - * - Temp file cleanup and recovery - * - Metadata management without organizing - * - * 4. SearchService.test.ts (13 tests) - * - Fuzzy search across filename, tags, path - * - Advanced filters (author:, copyright:) - * - Typo tolerance and similarity scoring - * - Metadata caching and index rebuilding - * - * Running Tests: - * -------------- - * - * # Run all tests - * npm test - * - * # Run specific service tests - * npm run test:database - * npm run test:tag - * npm run test:library - * npm run test:search - * - * Test Infrastructure: - * -------------------- - * - * - Framework: Node.js built-in test runner (node:test) - * - TypeScript: tsx for runtime execution - * - WAV Generation: WaveFile library with real audio data - * - Test Isolation: Temporary databases and libraries per suite - * - Audio Format: 16-bit PCM, 44100 Hz, sine wave (440 Hz) - * - Metadata: WAV INFO chunks (INAM, IART, ICOP, IRTD, etc.) - * - * Key Features: - * ------------- - * - * ✓ Real WAV file generation with embedded metadata - * ✓ Isolated test environments (no cross-contamination) - * ✓ Comprehensive coverage of all service methods - * ✓ Edge case testing (missing data, errors, duplicates) - * ✓ Fast execution with minimal dependencies - * ✓ No mocking - tests use actual service implementations - * - * For detailed documentation, see TEST_README.md - */ diff --git a/src/test/TagService.test.ts b/src/test/TagService.test.ts deleted file mode 100644 index f9704b9..0000000 --- a/src/test/TagService.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * Comprehensive test suite for TagService. - * Run with: node --import tsx --test src/test/TagService.test.ts - */ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert'; -import { TagService } from '../main/services/TagService'; -import { DatabaseService } from '../main/services/DatabaseService'; -import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -describe('TagService', () => { - let tagService: TagService; - let database: DatabaseService; - let dbPath: string; - let libraryPath: string; - let testFilePath: string; - - beforeEach(async () => { - dbPath = getTempDbPath(); - libraryPath = getTempLibraryPath(); - database = new DatabaseService(dbPath); - database.initialize(); - tagService = new TagService(database); - - await TestWavGenerator.createTestLibrary(libraryPath, [ - { - relativePath: 'test.wav', - options: { - duration: 1, - author: 'Test Author', - copyright: 'Test Copyright', - title: 'Test Title', - rating: 3, - tags: ['test', 'audio'], - categories: ['AMBNatr'] - } - } - ]); - - testFilePath = path.join(libraryPath, 'test.wav'); - - // Add file to database - database.upsertFile({ - absolutePath: testFilePath, - relativePath: 'test.wav', - fileName: 'test.wav', - displayName: 'test', - modifiedAt: Date.now(), - createdAt: Date.now(), - size: 1024, - durationMs: 1000, - sampleRate: 44100, - bitDepth: 16, - checksum: 'test-checksum', - tags: ['test'], - categories: ['AMBNatr'] - }); - }); - - afterEach(async () => { - database.close(); - await TestWavGenerator.cleanupTestLibrary(libraryPath); - await fs.unlink(dbPath).catch(() => {}); - }); - - describe('readMetadata', () => { - it('should read author from WAV file', () => { - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.author, 'Test Author'); - }); - - it('should read copyright from WAV file', () => { - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.copyright, 'Test Copyright'); - }); - - it('should read title from WAV file', () => { - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.title, 'Test Title'); - }); - - it('should read rating from WAV file', () => { - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.rating, 3); - }); - - it('should handle missing metadata gracefully', async () => { - const emptyFilePath = path.join(libraryPath, 'empty.wav'); - await TestWavGenerator.writeTestWav(emptyFilePath, {}); - - const metadata = tagService.readMetadata(emptyFilePath); - assert.strictEqual(metadata.author, undefined); - assert.strictEqual(metadata.copyright, undefined); - assert.strictEqual(metadata.title, undefined); - assert.strictEqual(metadata.rating, undefined); - }); - - it('should handle corrupted files gracefully', () => { - const metadata = tagService.readMetadata('/nonexistent/file.wav'); - assert.deepStrictEqual(metadata, {}); - }); - }); - - describe('writeMetadataOnly', () => { - it('should write author to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - author: 'New Author' - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.author, 'New Author'); - }); - - it('should write copyright to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - copyright: 'New Copyright' - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.copyright, 'New Copyright'); - }); - - it('should write title to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - title: 'New Title' - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.title, 'New Title'); - }); - - it('should write rating to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - rating: 5 - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.rating, 5); - }); - - it('should clear metadata when set to empty', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - author: '', - copyright: '', - title: '', - rating: 0 - }); - - const metadata = tagService.readMetadata(testFilePath); - assert.strictEqual(metadata.author, undefined); - assert.strictEqual(metadata.copyright, undefined); - assert.strictEqual(metadata.title, undefined); - assert.strictEqual(metadata.rating, undefined); - }); - - it('should write tags to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['new', 'tags'], - categories: ['AMBNatr'] - }); - - // Metadata write succeeded without throwing - assert.ok(true); - }); - - it('should write categories to WAV file', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBCity', 'AMBNatr'] - }); - - // Verify write succeeded - assert.ok(true); - }); - - it('should embed parent id in INFO chunk', () => { - tagService.writeMetadataOnly(testFilePath, { - tags: ['test'], - categories: ['AMBNatr'], - parentId: 42 - }); - - const info = tagService.readInfoTags(testFilePath); - assert.strictEqual(info.IPAR, '42'); - const comment = info.ICMT ?? ''; - assert.ok(comment.length > 0, 'Expected INFO comment to be populated with JSON payload'); - const parsed = JSON.parse(comment); - assert.strictEqual(parsed.parentId, 42); - assert.deepStrictEqual(parsed.categories, ['AMBNatr']); - }); - }); - - describe('applyTagging', () => { - it('should update tags in database', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const updated = tagService.applyTagging(fileId, ['new', 'tags'], ['AMBNatr']); - - assert.deepStrictEqual(updated.tags, ['new', 'tags']); - }); - - it('should update categories in database', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const updated = tagService.applyTagging(fileId, ['test'], ['AMBCity', 'AMBNatr']); - - assert.deepStrictEqual(updated.categories, ['AMBCity', 'AMBNatr']); - }); - - it('should write metadata to WAV file', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - tagService.applyTagging(fileId, ['new', 'tags'], ['AMBCity']); - - // Verify it didn't throw - assert.ok(true); - }); - - it('should normalize tags by trimming whitespace', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const updated = tagService.applyTagging(fileId, [' tag1 ', ' tag2 '], ['AMBNatr']); - - assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']); - }); - - it('should remove duplicate tags', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const updated = tagService.applyTagging(fileId, ['tag1', 'tag1', 'tag2'], ['AMBNatr']); - - assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']); - }); - - it('should remove empty tags', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const updated = tagService.applyTagging(fileId, ['tag1', '', ' ', 'tag2'], ['AMBNatr']); - - assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']); - }); - - it('should preserve existing tags when omitted', () => { - const files = database.listFiles(); - const fileId = files[0].id; - - const before = database.getFileById(fileId); - const updated = tagService.applyTagging(fileId, undefined, ['AMBNatr']); - - assert.deepStrictEqual(updated.tags, before.tags); - }); - }); -}); diff --git a/src/test/testHelpers.ts b/src/test/testHelpers.ts deleted file mode 100644 index 57cbf9b..0000000 --- a/src/test/testHelpers.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Test helpers and utilities for creating test WAV files with metadata. - */ -import { WaveFile } from 'wavefile'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -export class TestWavGenerator { - /** - * Creates a simple 1-second WAV file with metadata. - */ - public static createTestWav(options: { - duration?: number; - sampleRate?: number; - bitDepth?: number; - tags?: string[]; - categories?: string[]; - author?: string; - copyright?: string; - title?: string; - rating?: number; - } = {}): Buffer { - const { - duration = 1, - sampleRate = 44100, - bitDepth = 16, - tags = [], - categories = [], - author, - copyright, - title, - rating - } = options; - - // Generate simple sine wave data - const numSamples = Math.floor(sampleRate * duration); - const frequency = 440; // A4 note - const samples = new Int16Array(numSamples); - const amplitude = Math.pow(2, bitDepth - 1) - 1; - - for (let i = 0; i < numSamples; i++) { - const time = i / sampleRate; - samples[i] = Math.floor(amplitude * 0.5 * Math.sin(2 * Math.PI * frequency * time)); - } - - // Create WAV file - const wav = new WaveFile(); - wav.fromScratch(1, sampleRate, String(bitDepth), samples); - - // Add metadata using INFO chunks - const wavWithTags = wav as WaveFile & { - setTag?: (tag: string, value: string) => void; - }; - - if (wavWithTags.setTag) { - // Set tags and categories - if (tags.length > 0) { - wavWithTags.setTag('IKEY', tags.join('; ')); - wavWithTags.setTag('ISBJ', tags.join('; ')); - } - - if (categories.length > 0) { - wavWithTags.setTag('IGNR', categories.join('; ')); - wavWithTags.setTag('ICMT', categories.join('; ')); - wavWithTags.setTag('ISUB', categories.join('; ')); - } - - // Set metadata - if (title) { - wavWithTags.setTag('INAM', title); - } - - if (author) { - wavWithTags.setTag('IART', author); - } - - if (copyright) { - wavWithTags.setTag('ICOP', copyright); - } - - if (rating && rating > 0) { - wavWithTags.setTag('IRTD', String(rating * 2)); - } - - wavWithTags.setTag('ISFT', 'AudioSort Test Generator'); - } - - return Buffer.from(wav.toBuffer()); - } - - /** - * Writes a test WAV file to disk. - */ - public static async writeTestWav( - filePath: string, - options: Parameters[0] = {} - ): Promise { - const buffer = this.createTestWav(options); - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, buffer); - } - - /** - * Creates multiple test WAV files in a directory structure. - */ - public static async createTestLibrary( - rootPath: string, - files: Array<{ - relativePath: string; - options?: Parameters[0]; - }> - ): Promise { - await fs.mkdir(rootPath, { recursive: true }); - - for (const file of files) { - const fullPath = path.join(rootPath, file.relativePath); - await this.writeTestWav(fullPath, file.options); - } - } - - /** - * Cleans up a test directory. - */ - public static async cleanupTestLibrary(rootPath: string): Promise { - try { - await fs.rm(rootPath, { recursive: true, force: true }); - } catch (error) { - console.warn(`Failed to cleanup test library at ${rootPath}:`, error); - } - } -} - -/** - * Creates a temporary test database path. - */ -export function getTempDbPath(): string { - return path.join(process.cwd(), 'test-data', `test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`); -} - -/** - * Creates a temporary test library path. - */ -export function getTempLibraryPath(): string { - return path.join(process.cwd(), 'test-data', `test-lib-${Date.now()}-${Math.random().toString(36).slice(2)}`); -} - -/** - * Delays execution for testing async operations. - */ -export function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -}