mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
da638f9079
@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>
314 lines
12 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|