Files
Dragan Spiridonov da638f9079 feat(kernel): migrate NativeHnswBackend from @ruvector/router to hnswlib-node (#399)
@ruvector/router 0.1.28 was found to have four serious bugs that
together broke production code-intelligence semantic search and
forced the v3.9.5 hotfix to disable native HNSW entirely:

  1. HNSW search returned essentially random results — recall@10 was
     0% on the project's FNV fixture and 10% on textbook unit-Gaussian
     random vectors. The graph walk could not even find self-vectors
     when querying with a stored vector. Reproduced deterministically
     on linux-arm64; matches the linux-x64 GitHub Actions failures
     reported in #399.
  2. The VectorDb constructor unconditionally wrote a `vectors.db`
     redb file to the current working directory, polluting users'
     project roots with multi-MB files outside .agentic-qe/, in a
     non-SQLite format that no AQE migration or backup tooling knew
     about. Direct violation of the unified memory architecture.
  3. The redb file held a process-wide exclusive lock — only ONE
     VectorDb instance could exist per process. Subsequent constructors
     threw "Database error: Database already open. Cannot acquire lock."
  4. NAPI dispose did not synchronously release the redb lock (NAPI GC
     is not synchronous). The lock outlived dispose() and caused the
     v3.9.5 futex deadlock when the indexer tried to recreate after
     reset.

NativeHnswBackend is rewritten to wrap hnswlib-node@^3.0.0 — the
canonical Node.js binding for Yury Malkov's reference C++ Hnswlib
implementation, used by Pinecone, Weaviate, Qdrant, LangChain, and
ChromaDB. Empirical verification on the same fixture
(`scripts/diagnose-issue-399*.mjs`):

  - hnswlib-node:        recall@10 = 100%, top-1 = id 42 ✓
  - @ruvector/router:    recall@10 =  10%, top-1 = id 999 (random)

Insert is also faster (~324ms vs ~430ms for 1k vectors) and search
is comparable (0.35ms vs 0.21ms with correct vs wrong results).

No new dependency was added — `hnswlib-node` was already a direct
dependency in `package.json` from the legacy code path in
`src/integrations/embeddings/index/HNSWIndex.ts`.

Implementation notes:

  - Drops the local `vectorStore` mirror that the previous backend
    needed to re-score wrong native results — hnswlib-node returns
    correct distances directly.
  - Drops the `bruteForceSearch` private fallback — there is no
    fallback path because hnswlib-node always returns correct results.
  - `clear()` now recreates the underlying HierarchicalNSW from
    scratch instead of just markDelete-ing every label. The
    tombstone-only approach leaks state across long test runs that
    share the singleton HnswAdapter registry, producing duplicate
    labels from `searchKnn` after many cycles.
  - `search()` overshoots k by 2x and dedups by id as a defensive
    measure against any future hnswlib-node duplicate-label edge
    case, then truncates back to k.
  - `add()` doubles maxElements via `resizeIndex()` when the live id
    count fills the current capacity, so callers don't need to know
    the final index size at construction time.
  - `useNativeHNSW` default flipped back to `true`. Users with large
    codebases get sublinear HNSW search by default again.
  - All four `@ruvector/router` failure modes have explicit regression
    guards in the unit and integration test suites.

The two #399-skipped tests in `phase1-integration.test.ts`
(line 222 and 1185) are un-skipped and now pass green against the
new backend. The three flag-default tests that asserted
`useNativeHNSW: false` (broken by v3.9.5's hotfix) are updated to
assert `true` again.

Stale comments referencing `@ruvector/router`'s lock behavior are
updated in `src/kernel/unified-memory.ts`, `src/init/phases/06-code-intelligence.ts`,
`src/init/phases/10-workers.ts`, and `src/cli/index.ts`. The CLI
status output (`aqe ruvector status`) and the `useNativeHNSW` flag
description are updated to mention `hnswlib-node` instead of
`@ruvector/router`.

Refs #399, ADR-090.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 18:22:24 +00:00

314 lines
12 KiB
TypeScript

/**
* RuVector CLI Commands Tests
* Tests for aqe ruvector status/flags subcommands
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createRuVectorCommand } from '../../../../src/cli/commands/ruvector-commands.js';
import {
resetRuVectorFeatureFlags,
getRuVectorFeatureFlags,
setRuVectorFeatureFlags,
} from '../../../../src/integrations/ruvector/feature-flags.js';
import type { Command } from 'commander';
// ============================================================================
// Test Helpers
// ============================================================================
let mockConsoleLog: ReturnType<typeof vi.spyOn>;
let mockExit: ReturnType<typeof vi.spyOn>;
async function executeCommand(cmd: Command, args: string[]): Promise<void> {
cmd.exitOverride();
cmd.configureOutput({
writeOut: () => {},
writeErr: () => {},
});
await cmd.parseAsync(args, { from: 'user' });
}
function getLogOutput(): string {
return mockConsoleLog.mock.calls.map(call => call.join(' ')).join('\n');
}
// ============================================================================
// Tests
// ============================================================================
describe('RuVector CLI Commands', () => {
let command: ReturnType<typeof createRuVectorCommand>;
beforeEach(() => {
resetRuVectorFeatureFlags();
command = createRuVectorCommand();
mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {}) as never);
});
afterEach(() => {
resetRuVectorFeatureFlags();
vi.restoreAllMocks();
});
// --------------------------------------------------------------------------
// Command Structure
// --------------------------------------------------------------------------
describe('createRuVectorCommand', () => {
it('should create a command with name "ruvector"', () => {
expect(command.name()).toBe('ruvector');
});
it('should have a description', () => {
expect(command.description()).toBeTruthy();
expect(command.description()).toContain('RuVector');
});
it('should have status and flags subcommands', () => {
const subcommandNames = command.commands.map(c => c.name());
expect(subcommandNames).toContain('status');
expect(subcommandNames).toContain('flags');
});
});
// --------------------------------------------------------------------------
// Status Subcommand
// --------------------------------------------------------------------------
describe('status subcommand', () => {
it('should display native package information', async () => {
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).toContain('RuVector Integration Status');
expect(output).toContain('Native Packages');
// hnswlib-node replaced @ruvector/router for the native HNSW backend
// in #399 / ADR-090.
expect(output).toContain('hnswlib-node');
expect(output).toContain('prime-radiant-advanced-wasm');
expect(output).toContain('@ruvector/sona');
});
it('should display feature flag section', async () => {
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).toContain('Feature Flags');
expect(output).toContain('useNativeHNSW');
expect(output).toContain('useTemporalCompression');
expect(output).toContain('useMetadataFiltering');
expect(output).toContain('useDeterministicDither');
});
it('should show default marker for unmodified flags', async () => {
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).toContain('(default)');
});
it('should show modified marker when flag is changed', async () => {
// useNativeHNSW history: was flipped to false in v3.9.5 (deadlock fix),
// then flipped back to true after the #399 / ADR-090 hnswlib-node
// migration. The default is now true, so setting it to false here
// makes the value differ from the default and triggers the
// (modified) marker.
setRuVectorFeatureFlags({ useNativeHNSW: false });
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).toContain('(modified)');
});
it('should show compression details when temporal compression is enabled', async () => {
setRuVectorFeatureFlags({ useTemporalCompression: true });
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).toContain('Temporal Compression');
expect(output).toContain('8-bit');
expect(output).toContain('5-bit');
expect(output).toContain('3-bit');
});
it('should not show compression details when temporal compression is disabled', async () => {
setRuVectorFeatureFlags({ useTemporalCompression: false });
await executeCommand(command, ['status']);
const output = getLogOutput();
expect(output).not.toContain('Temporal Compression');
});
it('should show status for each native package', async () => {
await executeCommand(command, ['status']);
const output = getLogOutput();
// Packages may be installed or show fallback info.
// hnswlib-node and @ruvector/sona should be available.
expect(output).toContain('hnswlib-node');
expect(output).toContain('@ruvector/sona');
});
});
// --------------------------------------------------------------------------
// Flags Subcommand - Listing
// --------------------------------------------------------------------------
describe('flags subcommand - listing', () => {
it('should list all feature flags with defaults', async () => {
await executeCommand(command, ['flags']);
const output = getLogOutput();
expect(output).toContain('RuVector Feature Flags');
expect(output).toContain('useQESONA');
expect(output).toContain('useQEFlashAttention');
expect(output).toContain('useQEGNNIndex');
expect(output).toContain('logMigrationMetrics');
expect(output).toContain('useNativeHNSW');
expect(output).toContain('useTemporalCompression');
expect(output).toContain('useMetadataFiltering');
expect(output).toContain('useDeterministicDither');
});
it('should show descriptions for flags', async () => {
await executeCommand(command, ['flags']);
const output = getLogOutput();
expect(output).toContain('Self-Optimizing Neural Architecture');
expect(output).toContain('SIMD-accelerated attention');
});
it('should show profile hints', async () => {
await executeCommand(command, ['flags']);
const output = getLogOutput();
expect(output).toContain('performance');
expect(output).toContain('experimental');
expect(output).toContain('safe');
});
});
// --------------------------------------------------------------------------
// Flags Subcommand - Setting Flags
// --------------------------------------------------------------------------
describe('flags subcommand - setting', () => {
it('should set a flag to true', async () => {
setRuVectorFeatureFlags({ useNativeHNSW: false });
expect(getRuVectorFeatureFlags().useNativeHNSW).toBe(false);
await executeCommand(command, ['flags', '--set', 'useNativeHNSW=true']);
expect(getRuVectorFeatureFlags().useNativeHNSW).toBe(true);
const output = getLogOutput();
expect(output).toContain('useNativeHNSW');
});
it('should set a flag to false', async () => {
expect(getRuVectorFeatureFlags().useQESONA).toBe(true);
await executeCommand(command, ['flags', '--set', 'useQESONA=false']);
expect(getRuVectorFeatureFlags().useQESONA).toBe(false);
});
it('should reject unknown flag names', async () => {
await executeCommand(command, ['flags', '--set', 'unknownFlag=true']);
expect(mockExit).toHaveBeenCalledWith(1);
const output = getLogOutput();
expect(output).toContain('Unknown flag');
});
it('should reject invalid values', async () => {
await executeCommand(command, ['flags', '--set', 'useNativeHNSW=maybe']);
expect(mockExit).toHaveBeenCalledWith(1);
const output = getLogOutput();
expect(output).toContain('Invalid value');
});
it('should reject malformed set syntax', async () => {
await executeCommand(command, ['flags', '--set', 'invalidformat']);
expect(mockExit).toHaveBeenCalledWith(1);
const output = getLogOutput();
expect(output).toContain('Invalid format');
});
});
// --------------------------------------------------------------------------
// Flags Subcommand - Profiles
// --------------------------------------------------------------------------
describe('flags subcommand - profiles', () => {
it('should apply the performance profile', async () => {
await executeCommand(command, ['flags', '--profile', 'performance']);
const flags = getRuVectorFeatureFlags();
expect(flags.useNativeHNSW).toBe(true);
expect(flags.useTemporalCompression).toBe(true);
expect(flags.useDeterministicDither).toBe(true);
const output = getLogOutput();
expect(output).toContain('performance');
});
it('should apply the experimental profile', async () => {
await executeCommand(command, ['flags', '--profile', 'experimental']);
const flags = getRuVectorFeatureFlags();
expect(flags.useNativeHNSW).toBe(true);
expect(flags.useTemporalCompression).toBe(true);
expect(flags.useMetadataFiltering).toBe(true);
expect(flags.useDeterministicDither).toBe(true);
expect(flags.useQESONA).toBe(true);
expect(flags.useQEFlashAttention).toBe(true);
expect(flags.useQEGNNIndex).toBe(true);
expect(flags.logMigrationMetrics).toBe(true);
});
it('should apply the safe profile', async () => {
// First enable everything
setRuVectorFeatureFlags({
useNativeHNSW: true,
useTemporalCompression: true,
useMetadataFiltering: true,
useDeterministicDither: true,
});
await executeCommand(command, ['flags', '--profile', 'safe']);
const flags = getRuVectorFeatureFlags();
// Original 4 flags stay on
expect(flags.useQESONA).toBe(true);
expect(flags.useQEFlashAttention).toBe(true);
expect(flags.useQEGNNIndex).toBe(true);
expect(flags.logMigrationMetrics).toBe(true);
// New flags turned off by safe profile
expect(flags.useNativeHNSW).toBe(false);
expect(flags.useTemporalCompression).toBe(false);
expect(flags.useMetadataFiltering).toBe(false);
expect(flags.useDeterministicDither).toBe(false);
});
it('should reject unknown profile names', async () => {
await executeCommand(command, ['flags', '--profile', 'turbo']);
expect(mockExit).toHaveBeenCalledWith(1);
const output = getLogOutput();
expect(output).toContain('Unknown profile');
});
it('should display the flags being set by a profile', async () => {
await executeCommand(command, ['flags', '--profile', 'performance']);
const output = getLogOutput();
expect(output).toContain('useNativeHNSW');
expect(output).toContain('useTemporalCompression');
expect(output).toContain('useDeterministicDither');
});
});
});