import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartdb from '../ts/index.js'; import { MongoClient, Db } from 'mongodb'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; // --------------------------------------------------------------------------- // Test: Deletes persist across restart (tombstone + hint staleness detection) // Covers: append_tombstone to data.rdb, hint file data_file_size tracking, // stale hint detection on restart // --------------------------------------------------------------------------- let tmpDir: string; let localDb: smartdb.LocalSmartDb; let client: MongoClient; let db: Db; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-delete-test-')); } function cleanTmpDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } // ============================================================================ // Setup // ============================================================================ tap.test('setup: start local db and insert documents', async () => { tmpDir = makeTmpDir(); localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir }); const info = await localDb.start(); client = new MongoClient(info.connectionUri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); db = client.db('deletetest'); const coll = db.collection('items'); await coll.insertMany([ { name: 'keep-1', value: 100 }, { name: 'keep-2', value: 200 }, { name: 'delete-me', value: 999 }, { name: 'keep-3', value: 300 }, ]); const count = await coll.countDocuments(); expect(count).toEqual(4); }); // ============================================================================ // Delete and verify // ============================================================================ tap.test('delete-persistence: delete a document', async () => { const coll = db.collection('items'); const result = await coll.deleteOne({ name: 'delete-me' }); expect(result.deletedCount).toEqual(1); const remaining = await coll.countDocuments(); expect(remaining).toEqual(3); const deleted = await coll.findOne({ name: 'delete-me' }); expect(deleted).toBeNull(); }); // ============================================================================ // Graceful restart: delete survives // ============================================================================ tap.test('delete-persistence: graceful stop and restart', async () => { await client.close(); await localDb.stop(); // graceful — writes hint file localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir }); const info = await localDb.start(); client = new MongoClient(info.connectionUri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); db = client.db('deletetest'); }); tap.test('delete-persistence: deleted doc stays deleted after graceful restart', async () => { const coll = db.collection('items'); const count = await coll.countDocuments(); expect(count).toEqual(3); const deleted = await coll.findOne({ name: 'delete-me' }); expect(deleted).toBeNull(); // The remaining docs are intact const keep1 = await coll.findOne({ name: 'keep-1' }); expect(keep1).toBeTruthy(); expect(keep1!.value).toEqual(100); }); // ============================================================================ // Simulate ungraceful restart: delete after hint write, then restart // The hint file data_file_size check should detect the stale hint // ============================================================================ tap.test('delete-persistence: insert and delete more docs, then restart', async () => { const coll = db.collection('items'); // Insert a new doc await coll.insertOne({ name: 'temporary', value: 777 }); expect(await coll.countDocuments()).toEqual(4); // Delete it await coll.deleteOne({ name: 'temporary' }); expect(await coll.countDocuments()).toEqual(3); const gone = await coll.findOne({ name: 'temporary' }); expect(gone).toBeNull(); }); tap.test('delete-persistence: stop and restart again', async () => { await client.close(); await localDb.stop(); localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir }); const info = await localDb.start(); client = new MongoClient(info.connectionUri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); db = client.db('deletetest'); }); tap.test('delete-persistence: all deletes survived second restart', async () => { const coll = db.collection('items'); const count = await coll.countDocuments(); expect(count).toEqual(3); // Both deletes are permanent expect(await coll.findOne({ name: 'delete-me' })).toBeNull(); expect(await coll.findOne({ name: 'temporary' })).toBeNull(); // Survivors intact const names = (await coll.find({}).toArray()).map(d => d.name).sort(); expect(names).toEqual(['keep-1', 'keep-2', 'keep-3']); }); // ============================================================================ // Delete all docs and verify empty after restart // ============================================================================ tap.test('delete-persistence: delete all remaining docs', async () => { const coll = db.collection('items'); await coll.deleteMany({}); expect(await coll.countDocuments()).toEqual(0); }); tap.test('delete-persistence: restart with empty collection', async () => { await client.close(); await localDb.stop(); localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir }); const info = await localDb.start(); client = new MongoClient(info.connectionUri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); db = client.db('deletetest'); }); tap.test('delete-persistence: collection is empty after restart', async () => { const coll = db.collection('items'); const count = await coll.countDocuments(); expect(count).toEqual(0); }); // ============================================================================ // Cleanup // ============================================================================ tap.test('delete-persistence: cleanup', async () => { await client.close(); await localDb.stop(); cleanTmpDir(tmpDir); }); export default tap.start();