Files

319 lines
9.1 KiB
JavaScript

#!/usr/bin/env node
/**
* End-to-end RVF CLI smoke test.
*
* Tests the full lifecycle via `npx ruvector rvf` CLI commands:
* create -> ingest -> query -> restart simulation -> query -> verify match
*
* Exits with code 0 on success, code 1 on failure.
*
* Usage:
* node tests/rvf-integration/smoke-test.js
*/
'use strict';
const { execFileSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const DIM = 128;
const METRIC = 'cosine';
const VECTOR_COUNT = 20;
const K = 5;
// Locate the CLI entry point relative to the repo root.
const REPO_ROOT = path.resolve(__dirname, '..', '..');
const CLI_PATH = path.join(REPO_ROOT, 'npm', 'packages', 'ruvector', 'bin', 'cli.js');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
let tmpDir;
let storePath;
let inputPath;
let childPath;
let passed = 0;
let failed = 0;
/**
* Deterministic pseudo-random vector generation using an LCG.
* Matches the Rust `random_vector` function for cross-validation.
*/
function randomVector(dim, seed) {
const v = new Float64Array(dim);
let x = BigInt(seed) & 0xFFFFFFFFFFFFFFFFn;
for (let i = 0; i < dim; i++) {
x = (x * 6364136223846793005n + 1442695040888963407n) & 0xFFFFFFFFFFFFFFFFn;
v[i] = Number(x >> 33n) / 4294967295.0 - 0.5;
}
// Normalize for cosine.
let norm = 0;
for (let i = 0; i < dim; i++) norm += v[i] * v[i];
norm = Math.sqrt(norm);
const result = [];
for (let i = 0; i < dim; i++) result.push(norm > 1e-8 ? v[i] / norm : 0);
return result;
}
/**
* Run a CLI command and return stdout as a string.
* Throws on non-zero exit code.
*/
function runCli(args, opts = {}) {
const cmdArgs = ['node', CLI_PATH, 'rvf', ...args];
try {
const stdout = execFileSync(cmdArgs[0], cmdArgs.slice(1), {
cwd: REPO_ROOT,
timeout: 30000,
encoding: 'utf8',
env: {
...process.env,
// Disable chalk colors for easier parsing.
FORCE_COLOR: '0',
NO_COLOR: '1',
},
...opts,
});
return stdout.trim();
} catch (e) {
const stderr = e.stderr ? e.stderr.toString().trim() : '';
const stdout = e.stdout ? e.stdout.toString().trim() : '';
throw new Error(
`CLI failed (exit ${e.status}): ${args.join(' ')}\n` +
` stdout: ${stdout}\n` +
` stderr: ${stderr}`
);
}
}
/**
* Assert a condition and track pass/fail.
*/
function assert(condition, message) {
if (condition) {
passed++;
console.log(` PASS: ${message}`);
} else {
failed++;
console.error(` FAIL: ${message}`);
}
}
/**
* Assert that a function throws (CLI command fails).
*/
function assertThrows(fn, message) {
try {
fn();
failed++;
console.error(` FAIL: ${message} (expected error, got success)`);
} catch (_e) {
passed++;
console.log(` PASS: ${message}`);
}
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
function setup() {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rvf-smoke-'));
storePath = path.join(tmpDir, 'smoke.rvf');
inputPath = path.join(tmpDir, 'vectors.json');
childPath = path.join(tmpDir, 'child.rvf');
// Generate input vectors as JSON.
const entries = [];
for (let i = 0; i < VECTOR_COUNT; i++) {
const id = i + 1;
const vector = randomVector(DIM, id * 17 + 5);
entries.push({ id, vector });
}
fs.writeFileSync(inputPath, JSON.stringify(entries));
}
// ---------------------------------------------------------------------------
// Teardown
// ---------------------------------------------------------------------------
function teardown() {
try {
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
} catch (_e) {
// Best-effort cleanup.
}
}
// ---------------------------------------------------------------------------
// Test steps
// ---------------------------------------------------------------------------
function testCreate() {
console.log('\nStep 1: Create store');
const output = runCli(['create', storePath, '-d', String(DIM), '-m', METRIC]);
assert(output.includes('Created') || output.includes('created'), 'create reports success');
assert(fs.existsSync(storePath), 'store file exists on disk');
}
function testIngest() {
console.log('\nStep 2: Ingest vectors');
const output = runCli(['ingest', storePath, '-i', inputPath]);
assert(
output.includes('Ingested') || output.includes('accepted'),
'ingest reports accepted vectors'
);
}
function testQueryFirst() {
console.log('\nStep 3: Query (first pass)');
// Query with the vector for id=10 (seed = 9 * 17 + 5 = 158).
const queryVec = randomVector(DIM, 9 * 17 + 5);
const vecStr = queryVec.map(v => v.toFixed(8)).join(',');
const output = runCli(['query', storePath, '-v', vecStr, '-k', String(K)]);
assert(output.includes('result'), 'query returns results');
// Parse result count.
const countMatch = output.match(/(\d+)\s*result/);
if (countMatch) {
const count = parseInt(countMatch[1], 10);
assert(count > 0, `query returned ${count} results (> 0)`);
assert(count <= K, `query returned ${count} results (<= ${K})`);
} else {
assert(false, 'could not parse result count from output');
}
return output;
}
function testStatus() {
console.log('\nStep 4: Status check');
const output = runCli(['status', storePath]);
assert(output.includes('total_vectors') || output.includes('totalVectors'), 'status shows vector count');
}
function testSegments() {
console.log('\nStep 5: Segment listing');
const output = runCli(['segments', storePath]);
assert(
output.includes('segment') || output.includes('type='),
'segments command lists segments'
);
}
function testCompact() {
console.log('\nStep 6: Compact');
const output = runCli(['compact', storePath]);
assert(output.includes('Compact') || output.includes('compact'), 'compact reports completion');
}
function testDerive() {
console.log('\nStep 7: Derive child store');
const output = runCli(['derive', storePath, childPath]);
assert(
output.includes('Derived') || output.includes('derived'),
'derive reports success'
);
assert(fs.existsSync(childPath), 'child store file exists on disk');
}
function testChildSegments() {
console.log('\nStep 8: Child segment listing');
const output = runCli(['segments', childPath]);
assert(
output.includes('segment') || output.includes('type='),
'child segments command lists segments'
);
}
function testStatusAfterLifecycle() {
console.log('\nStep 9: Final status check');
const output = runCli(['status', storePath]);
assert(output.length > 0, 'status returns non-empty output');
}
function testExport() {
console.log('\nStep 10: Export');
const exportPath = path.join(tmpDir, 'export.json');
const output = runCli(['export', storePath, '-o', exportPath]);
assert(
output.includes('Exported') || output.includes('exported') || fs.existsSync(exportPath),
'export produces output file'
);
if (fs.existsSync(exportPath)) {
const data = JSON.parse(fs.readFileSync(exportPath, 'utf8'));
assert(data.status !== undefined, 'export contains status');
assert(data.segments !== undefined, 'export contains segments');
}
}
function testNonexistentStore() {
console.log('\nStep 11: Error handling');
assertThrows(
() => runCli(['status', '/tmp/nonexistent_smoke_test_rvf_99999.rvf']),
'status on nonexistent store fails with error'
);
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main() {
console.log('=== RVF CLI End-to-End Smoke Test ===');
console.log(` DIM=${DIM} METRIC=${METRIC} VECTORS=${VECTOR_COUNT} K=${K}`);
setup();
try {
// Check if CLI exists before running tests.
if (!fs.existsSync(CLI_PATH)) {
console.error(`\nCLI not found at: ${CLI_PATH}`);
console.error('Skipping CLI smoke test (CLI not built).');
console.log('\n=== SKIPPED (CLI not available) ===');
process.exit(0);
}
testCreate();
testIngest();
testQueryFirst();
testStatus();
testSegments();
testCompact();
testDerive();
testChildSegments();
testStatusAfterLifecycle();
testExport();
testNonexistentStore();
} catch (e) {
// If any step throws unexpectedly, we still want to report and clean up.
failed++;
console.error(`\nUNEXPECTED ERROR: ${e.message}`);
if (e.stack) console.error(e.stack);
} finally {
teardown();
}
// Summary.
const total = passed + failed;
console.log(`\n=== Results: ${passed}/${total} passed, ${failed} failed ===`);
if (failed > 0) {
process.exit(1);
} else {
console.log('All smoke tests passed.');
process.exit(0);
}
}
main();