ruvector-extensions
Version:
Advanced features for ruvector: embeddings, UI, exports, temporal tracking, and persistence
330 lines (265 loc) • 9.42 kB
text/typescript
/**
* Tests for Database Persistence Module
*
* This test suite covers:
* - Save and load operations
* - Snapshot management
* - Export/import functionality
* - Progress callbacks
* - Incremental saves
* - Error handling
* - Data integrity verification
*/
import { test } from 'node:test';
import { strictEqual, ok, deepStrictEqual } from 'node:assert';
import { promises as fs } from 'fs';
import * as path from 'path';
import { VectorDB } from 'ruvector';
import {
DatabasePersistence,
formatFileSize,
formatTimestamp,
estimateMemoryUsage,
} from '../src/persistence.js';
const TEST_DATA_DIR = './test-data';
// Cleanup helper
async function cleanup() {
try {
await fs.rm(TEST_DATA_DIR, { recursive: true, force: true });
} catch (error) {
// Ignore errors
}
}
// Create sample database
function createSampleDB(dimension = 128, count = 100) {
const db = new VectorDB({ dimension, metric: 'cosine' });
for (let i = 0; i < count; i++) {
db.insert({
id: `doc-${i}`,
vector: Array(dimension).fill(0).map(() => Math.random()),
metadata: {
index: i,
category: i % 3 === 0 ? 'A' : i % 3 === 1 ? 'B' : 'C',
timestamp: Date.now() - i * 1000,
},
});
}
return db;
}
// ============================================================================
// Test Suite
// ============================================================================
test('DatabasePersistence - Save and Load', async (t) => {
await cleanup();
const db = createSampleDB(128, 100);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'save-load'),
});
// Save
const savePath = await persistence.save();
ok(savePath, 'Save should return a path');
// Verify file exists
const stats = await fs.stat(savePath);
ok(stats.size > 0, 'Saved file should not be empty');
// Load into new database
const db2 = new VectorDB({ dimension: 128 });
const persistence2 = new DatabasePersistence(db2, {
baseDir: path.join(TEST_DATA_DIR, 'save-load'),
});
await persistence2.load({ path: savePath });
// Verify data
strictEqual(db2.stats().count, 100, 'Should load all vectors');
const original = db.get('doc-50');
const loaded = db2.get('doc-50');
ok(original && loaded, 'Should retrieve same document');
deepStrictEqual(loaded.metadata, original.metadata, 'Metadata should match');
});
test('DatabasePersistence - Compressed Save', async (t) => {
await cleanup();
const db = createSampleDB(128, 200);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'compressed'),
compression: 'gzip',
});
const savePath = await persistence.save({ compress: true });
// Verify compression
const compressedStats = await fs.stat(savePath);
// Save uncompressed for comparison
const persistence2 = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'uncompressed'),
compression: 'none',
});
const uncompressedPath = await persistence2.save({ compress: false });
const uncompressedStats = await fs.stat(uncompressedPath);
ok(
compressedStats.size < uncompressedStats.size,
'Compressed file should be smaller'
);
});
test('DatabasePersistence - Snapshot Management', async (t) => {
await cleanup();
const db = createSampleDB(64, 50);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'snapshots'),
maxSnapshots: 3,
});
// Create snapshots
const snap1 = await persistence.createSnapshot('snapshot-1', {
description: 'First snapshot',
});
ok(snap1.id, 'Snapshot should have ID');
strictEqual(snap1.name, 'snapshot-1', 'Snapshot name should match');
strictEqual(snap1.vectorCount, 50, 'Snapshot should record vector count');
// Add more vectors
for (let i = 50; i < 100; i++) {
db.insert({
id: `doc-${i}`,
vector: Array(64).fill(0).map(() => Math.random()),
});
}
const snap2 = await persistence.createSnapshot('snapshot-2');
strictEqual(snap2.vectorCount, 100, 'Second snapshot should have more vectors');
// List snapshots
const snapshots = await persistence.listSnapshots();
strictEqual(snapshots.length, 2, 'Should have 2 snapshots');
// Restore first snapshot
await persistence.restoreSnapshot(snap1.id);
strictEqual(db.stats().count, 50, 'Should restore to 50 vectors');
// Delete snapshot
await persistence.deleteSnapshot(snap1.id);
const remaining = await persistence.listSnapshots();
strictEqual(remaining.length, 1, 'Should have 1 snapshot after deletion');
});
test('DatabasePersistence - Export and Import', async (t) => {
await cleanup();
const db = createSampleDB(256, 150);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'export'),
});
const exportPath = path.join(TEST_DATA_DIR, 'export', 'database-export.json');
// Export
await persistence.export({
path: exportPath,
format: 'json',
compress: false,
});
// Verify export file
const exportStats = await fs.stat(exportPath);
ok(exportStats.size > 0, 'Export file should exist');
// Import into new database
const db2 = new VectorDB({ dimension: 256 });
const persistence2 = new DatabasePersistence(db2, {
baseDir: path.join(TEST_DATA_DIR, 'import'),
});
await persistence2.import({
path: exportPath,
clear: true,
verifyChecksum: true,
});
strictEqual(db2.stats().count, 150, 'Should import all vectors');
});
test('DatabasePersistence - Progress Callbacks', async (t) => {
await cleanup();
const db = createSampleDB(128, 300);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'progress'),
});
let progressCalls = 0;
let lastPercentage = 0;
await persistence.save({
onProgress: (progress) => {
progressCalls++;
ok(progress.percentage >= 0 && progress.percentage <= 100, 'Percentage should be 0-100');
ok(progress.percentage >= lastPercentage, 'Percentage should increase');
ok(progress.message, 'Should have progress message');
lastPercentage = progress.percentage;
},
});
ok(progressCalls > 0, 'Should call progress callback');
strictEqual(lastPercentage, 100, 'Should reach 100%');
});
test('DatabasePersistence - Checksum Verification', async (t) => {
await cleanup();
const db = createSampleDB(128, 100);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'checksum'),
});
const savePath = await persistence.save();
// Load with checksum verification
const db2 = new VectorDB({ dimension: 128 });
const persistence2 = new DatabasePersistence(db2, {
baseDir: path.join(TEST_DATA_DIR, 'checksum'),
});
// Should succeed with valid checksum
await persistence2.load({
path: savePath,
verifyChecksum: true,
});
strictEqual(db2.stats().count, 100, 'Should load successfully');
// Corrupt the file
const data = await fs.readFile(savePath, 'utf-8');
const corrupted = data.replace('"doc-50"', '"doc-XX"');
await fs.writeFile(savePath, corrupted);
// Should fail with corrupted file
const db3 = new VectorDB({ dimension: 128 });
const persistence3 = new DatabasePersistence(db3, {
baseDir: path.join(TEST_DATA_DIR, 'checksum'),
});
let errorThrown = false;
try {
await persistence3.load({
path: savePath,
verifyChecksum: true,
});
} catch (error) {
errorThrown = true;
ok(error.message.includes('checksum'), 'Should mention checksum in error');
}
ok(errorThrown, 'Should throw error for corrupted file');
});
test('Utility Functions', async (t) => {
// Test formatFileSize
strictEqual(formatFileSize(0), '0.00 B');
strictEqual(formatFileSize(1024), '1.00 KB');
strictEqual(formatFileSize(1024 * 1024), '1.00 MB');
strictEqual(formatFileSize(1536 * 1024), '1.50 MB');
// Test formatTimestamp
const timestamp = new Date('2024-01-15T10:30:00.000Z').getTime();
ok(formatTimestamp(timestamp).includes('2024-01-15'));
// Test estimateMemoryUsage
const state = {
version: '1.0.0',
options: { dimension: 128, metric: 'cosine' as const },
stats: { count: 100, dimension: 128, metric: 'cosine' },
vectors: Array(100).fill(null).map((_, i) => ({
id: `doc-${i}`,
vector: Array(128).fill(0),
metadata: { index: i },
})),
timestamp: Date.now(),
};
const usage = estimateMemoryUsage(state);
ok(usage > 0, 'Should estimate positive memory usage');
});
test('DatabasePersistence - Snapshot Cleanup', async (t) => {
await cleanup();
const db = createSampleDB(64, 50);
const persistence = new DatabasePersistence(db, {
baseDir: path.join(TEST_DATA_DIR, 'cleanup'),
maxSnapshots: 2,
});
// Create 4 snapshots
await persistence.createSnapshot('snap-1');
await persistence.createSnapshot('snap-2');
await persistence.createSnapshot('snap-3');
await persistence.createSnapshot('snap-4');
// Should only keep 2 most recent
const snapshots = await persistence.listSnapshots();
strictEqual(snapshots.length, 2, 'Should auto-cleanup old snapshots');
strictEqual(snapshots[0].name, 'snap-4', 'Should keep newest');
strictEqual(snapshots[1].name, 'snap-3', 'Should keep second newest');
});
// Cleanup after all tests
test.after(async () => {
await cleanup();
});