Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
288
npm/tests/unit/cli.test.js
Normal file
288
npm/tests/unit/cli.test.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Unit tests for ruvector CLI
|
||||
* Tests command execution, error handling, and output formatting
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const CLI_PATH = path.join(__dirname, '../../ruvector/bin/ruvector.js');
|
||||
const TEMP_DIR = path.join(__dirname, '../fixtures/temp');
|
||||
|
||||
// Setup and teardown
|
||||
test.before(() => {
|
||||
if (!fs.existsSync(TEMP_DIR)) {
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
// Cleanup temp files
|
||||
if (fs.existsSync(TEMP_DIR)) {
|
||||
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Test CLI availability
|
||||
test('CLI - Availability', async (t) => {
|
||||
await t.test('should have executable CLI script', () => {
|
||||
assert.ok(fs.existsSync(CLI_PATH), 'CLI script should exist');
|
||||
|
||||
const stats = fs.statSync(CLI_PATH);
|
||||
assert.ok(stats.isFile(), 'CLI should be a file');
|
||||
});
|
||||
|
||||
await t.test('should be executable', () => {
|
||||
try {
|
||||
// Check shebang
|
||||
const content = fs.readFileSync(CLI_PATH, 'utf-8');
|
||||
assert.ok(content.startsWith('#!/usr/bin/env node'), 'Should have Node.js shebang');
|
||||
} catch (error) {
|
||||
assert.fail(`Failed to read CLI file: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test info command
|
||||
test('CLI - Info Command', async (t) => {
|
||||
await t.test('should display backend information', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH} info`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
assert.ok(output, 'Should produce output');
|
||||
assert.ok(
|
||||
output.includes('Backend') || output.includes('Type'),
|
||||
'Should display backend type'
|
||||
);
|
||||
} catch (error) {
|
||||
// If command fails, check if it's due to missing dependencies
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true, 'Dependencies not available (expected)');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test help command
|
||||
test('CLI - Help Command', async (t) => {
|
||||
await t.test('should display help with no arguments', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH}`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
assert.ok(output.includes('Usage') || output.includes('Commands'), 'Should display help');
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('should display help with --help flag', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH} --help`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
assert.ok(output.includes('Usage') || output.includes('Commands'), 'Should display help');
|
||||
assert.ok(output.includes('info'), 'Should list info command');
|
||||
assert.ok(output.includes('init'), 'Should list init command');
|
||||
assert.ok(output.includes('search'), 'Should list search command');
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test version command
|
||||
test('CLI - Version Command', async (t) => {
|
||||
await t.test('should display version', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH} --version`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
assert.ok(output.trim().length > 0, 'Should output version');
|
||||
assert.ok(/\d+\.\d+\.\d+/.test(output), 'Should be in semver format');
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test init command
|
||||
test('CLI - Init Command', async (t) => {
|
||||
const indexPath = path.join(TEMP_DIR, 'test-index.bin');
|
||||
|
||||
await t.test('should initialize index with default options', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH} init ${indexPath}`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
output.includes('success') || output.includes('initialized'),
|
||||
'Should indicate success'
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
// Command might fail if backend not available, which is ok
|
||||
assert.ok(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('should initialize index with custom options', () => {
|
||||
try {
|
||||
const customPath = path.join(TEMP_DIR, 'custom-index.bin');
|
||||
const output = execSync(
|
||||
`node ${CLI_PATH} init ${customPath} --dimension 256 --metric euclidean --type hnsw`,
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
output.includes('256') && output.includes('euclidean'),
|
||||
'Should show custom options'
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
assert.ok(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test error handling
|
||||
test('CLI - Error Handling', async (t) => {
|
||||
await t.test('should handle unknown command gracefully', () => {
|
||||
try {
|
||||
execSync(`node ${CLI_PATH} unknown-command`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
assert.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
// Expected to fail
|
||||
assert.ok(true, 'Should reject unknown command');
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('should handle missing required arguments', () => {
|
||||
try {
|
||||
execSync(`node ${CLI_PATH} init`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
assert.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
// Expected to fail - missing path argument
|
||||
assert.ok(true, 'Should require path argument');
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('should handle invalid options', () => {
|
||||
try {
|
||||
const indexPath = path.join(TEMP_DIR, 'invalid-options.bin');
|
||||
execSync(`node ${CLI_PATH} init ${indexPath} --dimension invalid`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector'),
|
||||
stdio: 'pipe'
|
||||
});
|
||||
// May or may not fail depending on validation
|
||||
assert.ok(true);
|
||||
} catch (error) {
|
||||
// Expected behavior
|
||||
assert.ok(true, 'Should handle invalid dimension');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test output formatting
|
||||
test('CLI - Output Formatting', async (t) => {
|
||||
await t.test('should produce formatted output for info', () => {
|
||||
try {
|
||||
const output = execSync(`node ${CLI_PATH} info`, {
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector')
|
||||
});
|
||||
|
||||
// Check for formatting characters (tables, colors, etc.)
|
||||
// Even with colors stripped, should have structured output
|
||||
assert.ok(output.length > 10, 'Should have substantial output');
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module')) {
|
||||
console.log('⚠ Skipping CLI test - dependencies not installed');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test benchmark command
|
||||
test('CLI - Benchmark Command', async (t) => {
|
||||
await t.test('should run benchmark with default options', async () => {
|
||||
try {
|
||||
// Use smaller numbers for faster test
|
||||
const output = execSync(
|
||||
`node ${CLI_PATH} benchmark --dimension 64 --num-vectors 100 --num-queries 10`,
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
cwd: path.join(__dirname, '../../ruvector'),
|
||||
timeout: 30000 // 30 second timeout
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
output.includes('Insert') || output.includes('Search') || output.includes('benchmark'),
|
||||
'Should show benchmark results'
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.message.includes('Cannot find module') || error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
||||
console.log('⚠ Skipping CLI benchmark test - dependencies not installed or too much output');
|
||||
assert.ok(true);
|
||||
} else {
|
||||
assert.ok(true); // Backend might not be available
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
274
npm/tests/unit/core.test.js
Normal file
274
npm/tests/unit/core.test.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Unit tests for @ruvector/core package
|
||||
* Tests native bindings functionality
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
// Test platform detection and loading
|
||||
test('@ruvector/core - Platform Detection', async (t) => {
|
||||
await t.test('should detect current platform correctly', () => {
|
||||
const os = require('node:os');
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
assert.ok(['linux', 'darwin', 'win32'].includes(platform),
|
||||
`Platform ${platform} should be supported`);
|
||||
assert.ok(['x64', 'arm64'].includes(arch),
|
||||
`Architecture ${arch} should be supported`);
|
||||
});
|
||||
|
||||
await t.test('should load native binding for current platform', () => {
|
||||
try {
|
||||
const core = require('@ruvector/core');
|
||||
assert.ok(core, 'Core module should load');
|
||||
assert.ok(core.VectorDB, 'VectorDB class should be exported');
|
||||
assert.ok(typeof core.version === 'function', 'version function should be exported');
|
||||
assert.ok(typeof core.hello === 'function', 'hello function should be exported');
|
||||
} catch (error) {
|
||||
if (error.code === 'MODULE_NOT_FOUND') {
|
||||
assert.ok(true, 'Native binding not available (expected in some environments)');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test VectorDB creation and basic operations
|
||||
test('@ruvector/core - VectorDB Creation', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await t.test('should create VectorDB with dimensions', () => {
|
||||
const db = new core.VectorDB({ dimensions: 128 });
|
||||
assert.ok(db, 'VectorDB instance should be created');
|
||||
});
|
||||
|
||||
await t.test('should create VectorDB with full options', () => {
|
||||
const db = new core.VectorDB({
|
||||
dimensions: 256,
|
||||
distanceMetric: 'Cosine',
|
||||
hnswConfig: {
|
||||
m: 16,
|
||||
efConstruction: 200,
|
||||
efSearch: 100
|
||||
}
|
||||
});
|
||||
assert.ok(db, 'VectorDB with full config should be created');
|
||||
});
|
||||
|
||||
await t.test('should reject invalid dimensions', () => {
|
||||
assert.throws(
|
||||
() => new core.VectorDB({ dimensions: 0 }),
|
||||
/invalid.*dimension/i,
|
||||
'Should throw on zero dimensions'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Test vector operations
|
||||
test('@ruvector/core - Vector Operations', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new core.VectorDB({ dimensions });
|
||||
|
||||
await t.test('should insert vector and return ID', async () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.5);
|
||||
const id = await db.insert({ vector });
|
||||
|
||||
assert.ok(id, 'Should return an ID');
|
||||
assert.strictEqual(typeof id, 'string', 'ID should be a string');
|
||||
});
|
||||
|
||||
await t.test('should insert vector with custom ID', async () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.3);
|
||||
const customId = 'custom-id-123';
|
||||
const id = await db.insert({ id: customId, vector });
|
||||
|
||||
assert.strictEqual(id, customId, 'Should use custom ID');
|
||||
});
|
||||
|
||||
await t.test('should insert batch of vectors', async () => {
|
||||
const vectors = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `batch-${i}`,
|
||||
vector: new Float32Array(dimensions).fill(i / 10)
|
||||
}));
|
||||
|
||||
const ids = await db.insertBatch(vectors);
|
||||
|
||||
assert.strictEqual(ids.length, 10, 'Should return 10 IDs');
|
||||
assert.deepStrictEqual(ids, vectors.map(v => v.id), 'IDs should match');
|
||||
});
|
||||
|
||||
await t.test('should get vector count', async () => {
|
||||
const count = await db.len();
|
||||
assert.ok(count >= 12, `Should have at least 12 vectors, got ${count}`);
|
||||
});
|
||||
|
||||
await t.test('should check if empty', async () => {
|
||||
const isEmpty = await db.isEmpty();
|
||||
assert.strictEqual(isEmpty, false, 'Should not be empty');
|
||||
});
|
||||
});
|
||||
|
||||
// Test search operations
|
||||
test('@ruvector/core - Search Operations', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new core.VectorDB({
|
||||
dimensions,
|
||||
distanceMetric: 'Cosine'
|
||||
});
|
||||
|
||||
// Insert test vectors
|
||||
const testVectors = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `vec-${i}`,
|
||||
vector: new Float32Array(dimensions).map(() => Math.random())
|
||||
}));
|
||||
await db.insertBatch(testVectors);
|
||||
|
||||
await t.test('should search and return results', async () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = await db.search({ vector: query, k: 10 });
|
||||
|
||||
assert.ok(Array.isArray(results), 'Results should be an array');
|
||||
assert.ok(results.length > 0, 'Should return results');
|
||||
assert.ok(results.length <= 10, 'Should return at most k results');
|
||||
});
|
||||
|
||||
await t.test('search results should have correct structure', async () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = await db.search({ vector: query, k: 5 });
|
||||
|
||||
results.forEach(result => {
|
||||
assert.ok(result.id, 'Result should have ID');
|
||||
assert.strictEqual(typeof result.score, 'number', 'Score should be a number');
|
||||
assert.ok(result.score >= 0, 'Score should be non-negative');
|
||||
});
|
||||
});
|
||||
|
||||
await t.test('should respect k parameter', async () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = await db.search({ vector: query, k: 3 });
|
||||
|
||||
assert.ok(results.length <= 3, 'Should return at most 3 results');
|
||||
});
|
||||
|
||||
await t.test('results should be sorted by score', async () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = await db.search({ vector: query, k: 10 });
|
||||
|
||||
for (let i = 0; i < results.length - 1; i++) {
|
||||
assert.ok(
|
||||
results[i].score <= results[i + 1].score,
|
||||
'Results should be sorted by increasing distance'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test delete operations
|
||||
test('@ruvector/core - Delete Operations', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new core.VectorDB({ dimensions });
|
||||
|
||||
await t.test('should delete existing vector', async () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.5);
|
||||
const id = await db.insert({ id: 'to-delete', vector });
|
||||
|
||||
const deleted = await db.delete(id);
|
||||
assert.strictEqual(deleted, true, 'Should return true for deleted vector');
|
||||
});
|
||||
|
||||
await t.test('should return false for non-existent vector', async () => {
|
||||
const deleted = await db.delete('non-existent-id');
|
||||
assert.strictEqual(deleted, false, 'Should return false for non-existent vector');
|
||||
});
|
||||
});
|
||||
|
||||
// Test get operations
|
||||
test('@ruvector/core - Get Operations', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new core.VectorDB({ dimensions });
|
||||
|
||||
await t.test('should get existing vector', async () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.7);
|
||||
const id = await db.insert({ id: 'get-test', vector });
|
||||
|
||||
const entry = await db.get(id);
|
||||
assert.ok(entry, 'Should return entry');
|
||||
assert.strictEqual(entry.id, id, 'ID should match');
|
||||
assert.ok(entry.vector, 'Should have vector');
|
||||
});
|
||||
|
||||
await t.test('should return null for non-existent vector', async () => {
|
||||
const entry = await db.get('non-existent-id');
|
||||
assert.strictEqual(entry, null, 'Should return null for non-existent vector');
|
||||
});
|
||||
});
|
||||
|
||||
// Test version and utility functions
|
||||
test('@ruvector/core - Utility Functions', async (t) => {
|
||||
let core;
|
||||
|
||||
try {
|
||||
core = require('@ruvector/core');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping core tests - native binding not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await t.test('version should return string', () => {
|
||||
const version = core.version();
|
||||
assert.strictEqual(typeof version, 'string', 'Version should be a string');
|
||||
assert.ok(version.length > 0, 'Version should not be empty');
|
||||
});
|
||||
|
||||
await t.test('hello should return string', () => {
|
||||
const greeting = core.hello();
|
||||
assert.strictEqual(typeof greeting, 'string', 'Hello should return a string');
|
||||
assert.ok(greeting.length > 0, 'Greeting should not be empty');
|
||||
});
|
||||
});
|
||||
328
npm/tests/unit/ruvector.test.js
Normal file
328
npm/tests/unit/ruvector.test.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Unit tests for ruvector main package
|
||||
* Tests platform detection, fallback logic, and TypeScript types
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
// Test module loading and backend detection
|
||||
test('ruvector - Backend Detection', async (t) => {
|
||||
await t.test('should load ruvector module', () => {
|
||||
const ruvector = require('ruvector');
|
||||
assert.ok(ruvector, 'Module should load');
|
||||
assert.ok(ruvector.VectorIndex, 'VectorIndex should be exported');
|
||||
assert.ok(ruvector.getBackendInfo, 'getBackendInfo should be exported');
|
||||
assert.ok(ruvector.isNativeAvailable, 'isNativeAvailable should be exported');
|
||||
assert.ok(ruvector.Utils, 'Utils should be exported');
|
||||
});
|
||||
|
||||
await t.test('should detect backend type', () => {
|
||||
const { getBackendInfo } = require('ruvector');
|
||||
const info = getBackendInfo();
|
||||
|
||||
assert.ok(info, 'Should return backend info');
|
||||
assert.ok(['native', 'wasm'].includes(info.type), 'Backend type should be native or wasm');
|
||||
assert.ok(info.version, 'Should have version');
|
||||
assert.ok(Array.isArray(info.features), 'Features should be an array');
|
||||
});
|
||||
|
||||
await t.test('should check native availability', () => {
|
||||
const { isNativeAvailable } = require('ruvector');
|
||||
const hasNative = isNativeAvailable();
|
||||
|
||||
assert.strictEqual(typeof hasNative, 'boolean', 'Should return boolean');
|
||||
});
|
||||
|
||||
await t.test('should prioritize native over WASM when available', () => {
|
||||
const { getBackendInfo, isNativeAvailable } = require('ruvector');
|
||||
const info = getBackendInfo();
|
||||
const hasNative = isNativeAvailable();
|
||||
|
||||
if (hasNative) {
|
||||
assert.strictEqual(info.type, 'native', 'Should use native when available');
|
||||
assert.ok(
|
||||
info.features.includes('SIMD') || info.features.includes('Multi-threading'),
|
||||
'Native should have performance features'
|
||||
);
|
||||
} else {
|
||||
assert.strictEqual(info.type, 'wasm', 'Should fallback to WASM');
|
||||
assert.ok(
|
||||
info.features.includes('Browser-compatible'),
|
||||
'WASM should have browser compatibility'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test VectorIndex creation
|
||||
test('ruvector - VectorIndex Creation', async (t) => {
|
||||
const { VectorIndex } = require('ruvector');
|
||||
|
||||
await t.test('should create VectorIndex with options', () => {
|
||||
const index = new VectorIndex({
|
||||
dimension: 128,
|
||||
metric: 'cosine',
|
||||
indexType: 'hnsw'
|
||||
});
|
||||
|
||||
assert.ok(index, 'VectorIndex should be created');
|
||||
});
|
||||
|
||||
await t.test('should create VectorIndex with minimal options', () => {
|
||||
const index = new VectorIndex({
|
||||
dimension: 64
|
||||
});
|
||||
|
||||
assert.ok(index, 'VectorIndex with minimal options should be created');
|
||||
});
|
||||
|
||||
await t.test('should accept various index types', () => {
|
||||
const flatIndex = new VectorIndex({
|
||||
dimension: 128,
|
||||
indexType: 'flat'
|
||||
});
|
||||
|
||||
const hnswIndex = new VectorIndex({
|
||||
dimension: 128,
|
||||
indexType: 'hnsw'
|
||||
});
|
||||
|
||||
assert.ok(flatIndex, 'Flat index should be created');
|
||||
assert.ok(hnswIndex, 'HNSW index should be created');
|
||||
});
|
||||
});
|
||||
|
||||
// Test vector operations
|
||||
test('ruvector - Vector Operations', async (t) => {
|
||||
const { VectorIndex } = require('ruvector');
|
||||
const dimension = 128;
|
||||
const index = new VectorIndex({ dimension, metric: 'cosine' });
|
||||
|
||||
await t.test('should insert vector', async () => {
|
||||
await index.insert({
|
||||
id: 'test-1',
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
});
|
||||
|
||||
const stats = await index.stats();
|
||||
assert.ok(stats.vectorCount > 0, 'Should have vectors after insert');
|
||||
});
|
||||
|
||||
await t.test('should insert batch of vectors', async () => {
|
||||
const vectors = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `batch-${i}`,
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
}));
|
||||
|
||||
await index.insertBatch(vectors);
|
||||
|
||||
const stats = await index.stats();
|
||||
assert.ok(stats.vectorCount >= 10, 'Should have at least 10 vectors');
|
||||
});
|
||||
|
||||
await t.test('should insert batch with progress callback', async () => {
|
||||
const vectors = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `progress-${i}`,
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
}));
|
||||
|
||||
let progressCalled = false;
|
||||
await index.insertBatch(vectors, {
|
||||
batchSize: 5,
|
||||
progressCallback: (progress) => {
|
||||
progressCalled = true;
|
||||
assert.ok(progress >= 0 && progress <= 1, 'Progress should be between 0 and 1');
|
||||
}
|
||||
});
|
||||
|
||||
assert.ok(progressCalled, 'Progress callback should be called');
|
||||
});
|
||||
});
|
||||
|
||||
// Test search operations
|
||||
test('ruvector - Search Operations', async (t) => {
|
||||
const { VectorIndex } = require('ruvector');
|
||||
const dimension = 128;
|
||||
const index = new VectorIndex({ dimension, metric: 'cosine' });
|
||||
|
||||
// Insert test data
|
||||
const testVectors = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `search-test-${i}`,
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
}));
|
||||
await index.insertBatch(testVectors);
|
||||
|
||||
await t.test('should search vectors', async () => {
|
||||
const query = Array.from({ length: dimension }, () => Math.random());
|
||||
const results = await index.search(query, { k: 10 });
|
||||
|
||||
assert.ok(Array.isArray(results), 'Results should be an array');
|
||||
assert.ok(results.length > 0, 'Should return results');
|
||||
assert.ok(results.length <= 10, 'Should return at most k results');
|
||||
});
|
||||
|
||||
await t.test('should return results with correct structure', async () => {
|
||||
const query = Array.from({ length: dimension }, () => Math.random());
|
||||
const results = await index.search(query, { k: 5 });
|
||||
|
||||
results.forEach(result => {
|
||||
assert.ok(result.id, 'Result should have ID');
|
||||
assert.strictEqual(typeof result.score, 'number', 'Score should be a number');
|
||||
});
|
||||
});
|
||||
|
||||
await t.test('should respect k parameter', async () => {
|
||||
const query = Array.from({ length: dimension }, () => Math.random());
|
||||
const results = await index.search(query, { k: 3 });
|
||||
|
||||
assert.ok(results.length <= 3, 'Should return at most 3 results');
|
||||
});
|
||||
});
|
||||
|
||||
// Test delete and get operations
|
||||
test('ruvector - Delete and Get Operations', async (t) => {
|
||||
const { VectorIndex } = require('ruvector');
|
||||
const dimension = 128;
|
||||
const index = new VectorIndex({ dimension });
|
||||
|
||||
await t.test('should get vector by ID', async () => {
|
||||
const vector = {
|
||||
id: 'get-test',
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
};
|
||||
await index.insert(vector);
|
||||
|
||||
const retrieved = await index.get('get-test');
|
||||
assert.ok(retrieved, 'Should retrieve vector');
|
||||
assert.strictEqual(retrieved.id, 'get-test', 'ID should match');
|
||||
});
|
||||
|
||||
await t.test('should return null for non-existent ID', async () => {
|
||||
const retrieved = await index.get('non-existent');
|
||||
assert.strictEqual(retrieved, null, 'Should return null for non-existent ID');
|
||||
});
|
||||
|
||||
await t.test('should delete vector', async () => {
|
||||
const vector = {
|
||||
id: 'delete-test',
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
};
|
||||
await index.insert(vector);
|
||||
|
||||
const deleted = await index.delete('delete-test');
|
||||
assert.strictEqual(deleted, true, 'Should return true for deleted vector');
|
||||
|
||||
const retrieved = await index.get('delete-test');
|
||||
assert.strictEqual(retrieved, null, 'Deleted vector should not be retrievable');
|
||||
});
|
||||
});
|
||||
|
||||
// Test stats and utility operations
|
||||
test('ruvector - Stats and Utilities', async (t) => {
|
||||
const { VectorIndex } = require('ruvector');
|
||||
const dimension = 128;
|
||||
const index = new VectorIndex({ dimension });
|
||||
|
||||
await t.test('should return stats', async () => {
|
||||
const stats = await index.stats();
|
||||
|
||||
assert.ok(stats, 'Should return stats');
|
||||
assert.ok('vectorCount' in stats, 'Stats should have vectorCount');
|
||||
assert.ok('dimension' in stats, 'Stats should have dimension');
|
||||
assert.strictEqual(stats.dimension, dimension, 'Dimension should match');
|
||||
});
|
||||
|
||||
await t.test('should clear index', async () => {
|
||||
await index.insert({
|
||||
id: 'clear-test',
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
});
|
||||
|
||||
await index.clear();
|
||||
|
||||
const stats = await index.stats();
|
||||
assert.strictEqual(stats.vectorCount, 0, 'Index should be empty after clear');
|
||||
});
|
||||
|
||||
await t.test('should optimize index', async () => {
|
||||
// Insert some vectors
|
||||
const vectors = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `opt-${i}`,
|
||||
values: Array.from({ length: dimension }, () => Math.random())
|
||||
}));
|
||||
await index.insertBatch(vectors);
|
||||
|
||||
// Should not throw
|
||||
await index.optimize();
|
||||
assert.ok(true, 'Optimize should complete without error');
|
||||
});
|
||||
});
|
||||
|
||||
// Test Utils
|
||||
test('ruvector - Utils', async (t) => {
|
||||
const { Utils } = require('ruvector');
|
||||
|
||||
await t.test('should calculate cosine similarity', () => {
|
||||
const a = [1, 0, 0];
|
||||
const b = [1, 0, 0];
|
||||
const similarity = Utils.cosineSimilarity(a, b);
|
||||
|
||||
assert.strictEqual(similarity, 1, 'Identical vectors should have similarity 1');
|
||||
});
|
||||
|
||||
await t.test('should calculate cosine similarity for orthogonal vectors', () => {
|
||||
const a = [1, 0, 0];
|
||||
const b = [0, 1, 0];
|
||||
const similarity = Utils.cosineSimilarity(a, b);
|
||||
|
||||
assert.ok(Math.abs(similarity) < 0.001, 'Orthogonal vectors should have similarity ~0');
|
||||
});
|
||||
|
||||
await t.test('should throw on dimension mismatch for cosine', () => {
|
||||
assert.throws(
|
||||
() => Utils.cosineSimilarity([1, 2], [1, 2, 3]),
|
||||
/same dimension/i,
|
||||
'Should throw on dimension mismatch'
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('should calculate euclidean distance', () => {
|
||||
const a = [0, 0, 0];
|
||||
const b = [3, 4, 0];
|
||||
const distance = Utils.euclideanDistance(a, b);
|
||||
|
||||
assert.strictEqual(distance, 5, 'Distance should be 5');
|
||||
});
|
||||
|
||||
await t.test('should throw on dimension mismatch for euclidean', () => {
|
||||
assert.throws(
|
||||
() => Utils.euclideanDistance([1, 2], [1, 2, 3]),
|
||||
/same dimension/i,
|
||||
'Should throw on dimension mismatch'
|
||||
);
|
||||
});
|
||||
|
||||
await t.test('should normalize vector', () => {
|
||||
const vector = [3, 4];
|
||||
const normalized = Utils.normalize(vector);
|
||||
|
||||
assert.strictEqual(normalized[0], 0.6, 'First component should be 0.6');
|
||||
assert.strictEqual(normalized[1], 0.8, 'Second component should be 0.8');
|
||||
|
||||
// Check magnitude is 1
|
||||
const magnitude = Math.sqrt(normalized[0] ** 2 + normalized[1] ** 2);
|
||||
assert.ok(Math.abs(magnitude - 1) < 0.001, 'Normalized vector should have magnitude 1');
|
||||
});
|
||||
|
||||
await t.test('should generate random vector', () => {
|
||||
const dimension = 128;
|
||||
const vector = Utils.randomVector(dimension);
|
||||
|
||||
assert.strictEqual(vector.length, dimension, 'Should have correct dimension');
|
||||
|
||||
// Check it's normalized
|
||||
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
||||
assert.ok(Math.abs(magnitude - 1) < 0.001, 'Random vector should be normalized');
|
||||
});
|
||||
});
|
||||
286
npm/tests/unit/wasm.test.js
Normal file
286
npm/tests/unit/wasm.test.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Unit tests for @ruvector/wasm package
|
||||
* Tests WebAssembly bindings functionality
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
// Test WASM module loading
|
||||
test('@ruvector/wasm - Module Loading', async (t) => {
|
||||
await t.test('should load WASM module in Node.js', async () => {
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
assert.ok(wasm, 'WASM module should load');
|
||||
assert.ok(wasm.VectorDB, 'VectorDB class should be exported');
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
console.log('⚠ WASM module not built yet - run build:wasm first');
|
||||
assert.ok(true, 'WASM not available (expected)');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('should detect environment correctly', () => {
|
||||
const isNode = typeof process !== 'undefined' &&
|
||||
process.versions != null &&
|
||||
process.versions.node != null;
|
||||
assert.strictEqual(isNode, true, 'Should detect Node.js environment');
|
||||
});
|
||||
});
|
||||
|
||||
// Test VectorDB creation
|
||||
test('@ruvector/wasm - VectorDB Creation', async (t) => {
|
||||
let VectorDB;
|
||||
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
VectorDB = wasm.VectorDB;
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await t.test('should create VectorDB instance', async () => {
|
||||
const db = new VectorDB({ dimensions: 128 });
|
||||
await db.init();
|
||||
assert.ok(db, 'VectorDB instance should be created');
|
||||
});
|
||||
|
||||
await t.test('should create VectorDB with options', async () => {
|
||||
const db = new VectorDB({
|
||||
dimensions: 256,
|
||||
metric: 'cosine',
|
||||
useHnsw: true
|
||||
});
|
||||
await db.init();
|
||||
assert.ok(db, 'VectorDB with options should be created');
|
||||
});
|
||||
|
||||
await t.test('should require init before use', async () => {
|
||||
const db = new VectorDB({ dimensions: 128 });
|
||||
|
||||
assert.throws(
|
||||
() => db.insert(new Float32Array(128)),
|
||||
/not initialized/i,
|
||||
'Should throw when not initialized'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Test vector operations
|
||||
test('@ruvector/wasm - Vector Operations', async (t) => {
|
||||
let VectorDB;
|
||||
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
VectorDB = wasm.VectorDB;
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new VectorDB({ dimensions });
|
||||
await db.init();
|
||||
|
||||
await t.test('should insert vector', () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.5);
|
||||
const id = db.insert(vector);
|
||||
|
||||
assert.ok(id, 'Should return an ID');
|
||||
assert.strictEqual(typeof id, 'string', 'ID should be a string');
|
||||
});
|
||||
|
||||
await t.test('should insert vector with custom ID', () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.3);
|
||||
const customId = 'wasm-custom-id';
|
||||
const id = db.insert(vector, customId);
|
||||
|
||||
assert.strictEqual(id, customId, 'Should use custom ID');
|
||||
});
|
||||
|
||||
await t.test('should insert vector with metadata', () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.3);
|
||||
const metadata = { label: 'test', value: 42 };
|
||||
const id = db.insert(vector, 'with-meta', metadata);
|
||||
|
||||
assert.ok(id, 'Should return ID');
|
||||
});
|
||||
|
||||
await t.test('should insert batch of vectors', () => {
|
||||
const vectors = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `wasm-batch-${i}`,
|
||||
vector: new Float32Array(dimensions).fill(i / 10)
|
||||
}));
|
||||
|
||||
const ids = db.insertBatch(vectors);
|
||||
|
||||
assert.strictEqual(ids.length, 10, 'Should return 10 IDs');
|
||||
});
|
||||
|
||||
await t.test('should accept array as vector', () => {
|
||||
const vector = Array.from({ length: dimensions }, () => Math.random());
|
||||
const id = db.insert(vector);
|
||||
|
||||
assert.ok(id, 'Should accept array and return ID');
|
||||
});
|
||||
|
||||
await t.test('should get vector count', () => {
|
||||
const count = db.len();
|
||||
assert.ok(count > 0, `Should have vectors, got ${count}`);
|
||||
});
|
||||
|
||||
await t.test('should check if empty', () => {
|
||||
const isEmpty = db.isEmpty();
|
||||
assert.strictEqual(isEmpty, false, 'Should not be empty');
|
||||
});
|
||||
|
||||
await t.test('should get dimensions', () => {
|
||||
const dims = db.getDimensions();
|
||||
assert.strictEqual(dims, dimensions, 'Dimensions should match');
|
||||
});
|
||||
});
|
||||
|
||||
// Test search operations
|
||||
test('@ruvector/wasm - Search Operations', async (t) => {
|
||||
let VectorDB;
|
||||
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
VectorDB = wasm.VectorDB;
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new VectorDB({ dimensions, metric: 'cosine' });
|
||||
await db.init();
|
||||
|
||||
// Insert test vectors
|
||||
const testVectors = Array.from({ length: 50 }, (_, i) => ({
|
||||
id: `wasm-vec-${i}`,
|
||||
vector: new Float32Array(dimensions).map(() => Math.random())
|
||||
}));
|
||||
db.insertBatch(testVectors);
|
||||
|
||||
await t.test('should search and return results', () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = db.search(query, 10);
|
||||
|
||||
assert.ok(Array.isArray(results), 'Results should be an array');
|
||||
assert.ok(results.length > 0, 'Should return results');
|
||||
assert.ok(results.length <= 10, 'Should return at most k results');
|
||||
});
|
||||
|
||||
await t.test('search results should have correct structure', () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = db.search(query, 5);
|
||||
|
||||
results.forEach(result => {
|
||||
assert.ok(result.id, 'Result should have ID');
|
||||
assert.strictEqual(typeof result.score, 'number', 'Score should be a number');
|
||||
});
|
||||
});
|
||||
|
||||
await t.test('should accept array as query', () => {
|
||||
const query = Array.from({ length: dimensions }, () => Math.random());
|
||||
const results = db.search(query, 5);
|
||||
|
||||
assert.ok(Array.isArray(results), 'Should accept array and return results');
|
||||
});
|
||||
|
||||
await t.test('should respect k parameter', () => {
|
||||
const query = new Float32Array(dimensions).fill(0.5);
|
||||
const results = db.search(query, 3);
|
||||
|
||||
assert.ok(results.length <= 3, 'Should return at most 3 results');
|
||||
});
|
||||
});
|
||||
|
||||
// Test delete operations
|
||||
test('@ruvector/wasm - Delete Operations', async (t) => {
|
||||
let VectorDB;
|
||||
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
VectorDB = wasm.VectorDB;
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new VectorDB({ dimensions });
|
||||
await db.init();
|
||||
|
||||
await t.test('should delete existing vector', () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.5);
|
||||
const id = db.insert(vector, 'wasm-to-delete');
|
||||
|
||||
const deleted = db.delete(id);
|
||||
assert.strictEqual(deleted, true, 'Should return true for deleted vector');
|
||||
});
|
||||
|
||||
await t.test('should return false for non-existent vector', () => {
|
||||
const deleted = db.delete('wasm-non-existent');
|
||||
assert.strictEqual(deleted, false, 'Should return false for non-existent vector');
|
||||
});
|
||||
});
|
||||
|
||||
// Test get operations
|
||||
test('@ruvector/wasm - Get Operations', async (t) => {
|
||||
let VectorDB;
|
||||
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
VectorDB = wasm.VectorDB;
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = 128;
|
||||
const db = new VectorDB({ dimensions });
|
||||
await db.init();
|
||||
|
||||
await t.test('should get existing vector', () => {
|
||||
const vector = new Float32Array(dimensions).fill(0.7);
|
||||
const id = db.insert(vector, 'wasm-get-test');
|
||||
|
||||
const entry = db.get(id);
|
||||
assert.ok(entry, 'Should return entry');
|
||||
assert.strictEqual(entry.id, id, 'ID should match');
|
||||
assert.ok(entry.vector, 'Should have vector');
|
||||
});
|
||||
|
||||
await t.test('should return null for non-existent vector', () => {
|
||||
const entry = db.get('wasm-non-existent');
|
||||
assert.strictEqual(entry, null, 'Should return null for non-existent vector');
|
||||
});
|
||||
});
|
||||
|
||||
// Test utility functions
|
||||
test('@ruvector/wasm - Utility Functions', async (t) => {
|
||||
let wasm;
|
||||
|
||||
try {
|
||||
wasm = await import('@ruvector/wasm');
|
||||
} catch (error) {
|
||||
console.log('⚠ Skipping WASM tests - module not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await t.test('should detect SIMD support', async () => {
|
||||
const hasSIMD = await wasm.detectSIMD();
|
||||
assert.strictEqual(typeof hasSIMD, 'boolean', 'Should return boolean');
|
||||
});
|
||||
|
||||
await t.test('should return version', async () => {
|
||||
const version = await wasm.version();
|
||||
assert.strictEqual(typeof version, 'string', 'Version should be a string');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user