import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartmongo from '../ts/index.js'; import * as path from 'path'; import * as fs from 'fs/promises'; const { WAL, ObjectId } = smartmongo.tsmdb; let wal: InstanceType; const TEST_WAL_PATH = '/tmp/tsmdb-test-wal/test.wal'; // Helper to clean up test files async function cleanupTestFiles() { try { await fs.rm('/tmp/tsmdb-test-wal', { recursive: true, force: true }); } catch { // Ignore if doesn't exist } } // ============================================================================ // Setup // ============================================================================ tap.test('wal: cleanup before tests', async () => { await cleanupTestFiles(); }); tap.test('wal: should create WAL instance', async () => { wal = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 }); expect(wal).toBeTruthy(); }); tap.test('wal: should initialize WAL', async () => { const result = await wal.initialize(); expect(result).toBeTruthy(); expect(result.recoveredEntries).toBeArray(); expect(result.recoveredEntries.length).toEqual(0); }); // ============================================================================ // LSN Tests // ============================================================================ tap.test('wal: getCurrentLsn should return 0 initially', async () => { const lsn = wal.getCurrentLsn(); expect(lsn).toEqual(0); }); tap.test('wal: LSN should increment after logging', async () => { const doc = { _id: new ObjectId(), name: 'Test' }; const lsn = await wal.logInsert('testdb', 'testcoll', doc as any); expect(lsn).toEqual(1); expect(wal.getCurrentLsn()).toEqual(1); }); // ============================================================================ // Insert Logging Tests // ============================================================================ tap.test('wal: logInsert should create entry with correct structure', async () => { const doc = { _id: new ObjectId(), name: 'InsertTest', value: 42 }; const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any); expect(lsn).toBeGreaterThan(0); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('insert'); expect(entry!.dbName).toEqual('testdb'); expect(entry!.collName).toEqual('insertcoll'); expect(entry!.documentId).toEqual(doc._id.toHexString()); expect(entry!.data).toBeTruthy(); expect(entry!.timestamp).toBeGreaterThan(0); expect(entry!.checksum).toBeGreaterThan(0); }); tap.test('wal: logInsert with transaction ID', async () => { const doc = { _id: new ObjectId(), name: 'TxnInsertTest' }; const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any, 'txn-123'); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry!.txnId).toEqual('txn-123'); }); // ============================================================================ // Update Logging Tests // ============================================================================ tap.test('wal: logUpdate should store old and new document', async () => { const oldDoc = { _id: new ObjectId(), name: 'OldName', value: 1 }; const newDoc = { ...oldDoc, name: 'NewName', value: 2 }; const lsn = await wal.logUpdate('testdb', 'updatecoll', oldDoc as any, newDoc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('update'); expect(entry!.data).toBeTruthy(); expect(entry!.previousData).toBeTruthy(); expect(entry!.documentId).toEqual(oldDoc._id.toHexString()); }); // ============================================================================ // Delete Logging Tests // ============================================================================ tap.test('wal: logDelete should record deleted document', async () => { const doc = { _id: new ObjectId(), name: 'ToDelete' }; const lsn = await wal.logDelete('testdb', 'deletecoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('delete'); expect(entry!.previousData).toBeTruthy(); expect(entry!.data).toBeUndefined(); }); // ============================================================================ // Transaction Logging Tests // ============================================================================ tap.test('wal: logBeginTransaction should create begin entry', async () => { const lsn = await wal.logBeginTransaction('txn-begin-test'); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('begin'); expect(entry!.txnId).toEqual('txn-begin-test'); }); tap.test('wal: logCommitTransaction should create commit entry', async () => { const lsn = await wal.logCommitTransaction('txn-commit-test'); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('commit'); expect(entry!.txnId).toEqual('txn-commit-test'); }); tap.test('wal: logAbortTransaction should create abort entry', async () => { const lsn = await wal.logAbortTransaction('txn-abort-test'); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry).toBeTruthy(); expect(entry!.operation).toEqual('abort'); expect(entry!.txnId).toEqual('txn-abort-test'); }); // ============================================================================ // getTransactionEntries Tests // ============================================================================ tap.test('wal: getTransactionEntries should return entries for transaction', async () => { // Log a complete transaction const txnId = 'txn-entries-test'; await wal.logBeginTransaction(txnId); const doc1 = { _id: new ObjectId(), name: 'TxnDoc1' }; await wal.logInsert('testdb', 'txncoll', doc1 as any, txnId); const doc2 = { _id: new ObjectId(), name: 'TxnDoc2' }; await wal.logInsert('testdb', 'txncoll', doc2 as any, txnId); await wal.logCommitTransaction(txnId); const entries = wal.getTransactionEntries(txnId); expect(entries.length).toEqual(4); // begin + 2 inserts + commit expect(entries.every(e => e.txnId === txnId)).toBeTrue(); }); tap.test('wal: getTransactionEntries should return empty for unknown transaction', async () => { const entries = wal.getTransactionEntries('unknown-txn-id'); expect(entries.length).toEqual(0); }); // ============================================================================ // getEntriesAfter Tests // ============================================================================ tap.test('wal: getEntriesAfter should filter by LSN', async () => { const currentLsn = wal.getCurrentLsn(); // Add more entries const doc = { _id: new ObjectId(), name: 'AfterTest' }; await wal.logInsert('testdb', 'aftercoll', doc as any); const entries = wal.getEntriesAfter(currentLsn); expect(entries.length).toEqual(1); expect(entries[0].lsn).toBeGreaterThan(currentLsn); }); tap.test('wal: getEntriesAfter with LSN 0 should return all entries', async () => { const entries = wal.getEntriesAfter(0); expect(entries.length).toBeGreaterThan(0); expect(entries.length).toEqual(wal.getCurrentLsn()); }); // ============================================================================ // Checkpoint Tests // ============================================================================ tap.test('wal: checkpoint should create checkpoint entry', async () => { const lsn = await wal.checkpoint(); expect(lsn).toBeGreaterThan(0); // After checkpoint, getEntriesAfter(checkpoint) should be limited const entries = wal.getEntriesAfter(0); expect(entries.some(e => e.operation === 'checkpoint')).toBeTrue(); }); // ============================================================================ // Document Recovery Tests // ============================================================================ tap.test('wal: recoverDocument should deserialize document from entry', async () => { const originalDoc = { _id: new ObjectId(), name: 'RecoverTest', nested: { a: 1, b: 2 } }; const lsn = await wal.logInsert('testdb', 'recovercoll', originalDoc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered).toBeTruthy(); expect(recovered!.name).toEqual('RecoverTest'); expect(recovered!.nested.a).toEqual(1); expect(recovered!.nested.b).toEqual(2); }); tap.test('wal: recoverDocument should return null for entry without data', async () => { const lsn = await wal.logBeginTransaction('recover-no-data'); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered).toBeNull(); }); tap.test('wal: recoverPreviousDocument should deserialize previous state', async () => { const oldDoc = { _id: new ObjectId(), name: 'Old', value: 100 }; const newDoc = { ...oldDoc, name: 'New', value: 200 }; const lsn = await wal.logUpdate('testdb', 'recovercoll', oldDoc as any, newDoc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const previous = wal.recoverPreviousDocument(entry!); expect(previous).toBeTruthy(); expect(previous!.name).toEqual('Old'); expect(previous!.value).toEqual(100); }); tap.test('wal: recoverPreviousDocument should return null for insert entry', async () => { const doc = { _id: new ObjectId(), name: 'NoPrevious' }; const lsn = await wal.logInsert('testdb', 'recovercoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const previous = wal.recoverPreviousDocument(entry!); expect(previous).toBeNull(); }); // ============================================================================ // WAL Persistence and Recovery Tests // ============================================================================ tap.test('wal: should persist and recover entries', async () => { // Close current WAL await wal.close(); // Create new WAL instance and initialize (should recover) const wal2 = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 }); const result = await wal2.initialize(); // Should have recovered entries expect(result.recoveredEntries).toBeArray(); // After checkpoint, there might not be many recoverable entries // but getCurrentLsn should be preserved or reset await wal2.close(); }); // ============================================================================ // Entry Checksum Tests // ============================================================================ tap.test('wal: entries should have valid checksums', async () => { wal = new WAL(TEST_WAL_PATH + '.checksum', { checkpointInterval: 100 }); await wal.initialize(); const doc = { _id: new ObjectId(), name: 'ChecksumTest' }; const lsn = await wal.logInsert('testdb', 'checksumcoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); expect(entry!.checksum).toBeGreaterThan(0); expect(typeof entry!.checksum).toEqual('number'); }); // ============================================================================ // Edge Cases // ============================================================================ tap.test('wal: should handle special characters in document', async () => { const doc = { _id: new ObjectId(), name: 'Test\nWith\tSpecial\r\nChars', emoji: '🎉', unicode: '日本語', }; const lsn = await wal.logInsert('testdb', 'specialcoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered!.name).toEqual('Test\nWith\tSpecial\r\nChars'); expect(recovered!.emoji).toEqual('🎉'); expect(recovered!.unicode).toEqual('日本語'); }); tap.test('wal: should handle binary data in documents', async () => { const doc = { _id: new ObjectId(), binaryField: Buffer.from([0x00, 0xFF, 0x7F, 0x80]), }; const lsn = await wal.logInsert('testdb', 'binarycoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered).toBeTruthy(); }); tap.test('wal: should handle nested documents', async () => { const doc = { _id: new ObjectId(), level1: { level2: { level3: { value: 'deep', }, }, }, }; const lsn = await wal.logInsert('testdb', 'nestedcoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered!.level1.level2.level3.value).toEqual('deep'); }); tap.test('wal: should handle arrays in documents', async () => { const doc = { _id: new ObjectId(), tags: ['a', 'b', 'c'], numbers: [1, 2, 3], mixed: [1, 'two', { three: 3 }], }; const lsn = await wal.logInsert('testdb', 'arraycoll', doc as any); const entries = wal.getEntriesAfter(lsn - 1); const entry = entries.find(e => e.lsn === lsn); const recovered = wal.recoverDocument(entry!); expect(recovered!.tags).toEqual(['a', 'b', 'c']); expect(recovered!.numbers).toEqual([1, 2, 3]); expect(recovered!.mixed[2].three).toEqual(3); }); // ============================================================================ // Cleanup // ============================================================================ tap.test('wal: cleanup', async () => { await wal.close(); await cleanupTestFiles(); expect(true).toBeTrue(); }); export default tap.start();