@sparkleideas/plugins 3.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +401 -0
  2. package/__tests__/collection-manager.test.ts +332 -0
  3. package/__tests__/dependency-graph.test.ts +434 -0
  4. package/__tests__/enhanced-plugin-registry.test.ts +488 -0
  5. package/__tests__/plugin-registry.test.ts +368 -0
  6. package/__tests__/ruvector-bridge.test.ts +2429 -0
  7. package/__tests__/ruvector-integration.test.ts +1602 -0
  8. package/__tests__/ruvector-migrations.test.ts +1099 -0
  9. package/__tests__/ruvector-quantization.test.ts +846 -0
  10. package/__tests__/ruvector-streaming.test.ts +1088 -0
  11. package/__tests__/sdk.test.ts +325 -0
  12. package/__tests__/security.test.ts +348 -0
  13. package/__tests__/utils/ruvector-test-utils.ts +860 -0
  14. package/examples/plugin-creator/index.ts +636 -0
  15. package/examples/plugin-creator/plugin-creator.test.ts +312 -0
  16. package/examples/ruvector/README.md +288 -0
  17. package/examples/ruvector/attention-patterns.ts +394 -0
  18. package/examples/ruvector/basic-usage.ts +288 -0
  19. package/examples/ruvector/docker-compose.yml +75 -0
  20. package/examples/ruvector/gnn-analysis.ts +501 -0
  21. package/examples/ruvector/hyperbolic-hierarchies.ts +557 -0
  22. package/examples/ruvector/init-db.sql +119 -0
  23. package/examples/ruvector/quantization.ts +680 -0
  24. package/examples/ruvector/self-learning.ts +447 -0
  25. package/examples/ruvector/semantic-search.ts +576 -0
  26. package/examples/ruvector/streaming-large-data.ts +507 -0
  27. package/examples/ruvector/transactions.ts +594 -0
  28. package/examples/ruvector-plugins/hook-pattern-library.ts +486 -0
  29. package/examples/ruvector-plugins/index.ts +79 -0
  30. package/examples/ruvector-plugins/intent-router.ts +354 -0
  31. package/examples/ruvector-plugins/mcp-tool-optimizer.ts +424 -0
  32. package/examples/ruvector-plugins/reasoning-bank.ts +657 -0
  33. package/examples/ruvector-plugins/ruvector-plugins.test.ts +518 -0
  34. package/examples/ruvector-plugins/semantic-code-search.ts +498 -0
  35. package/examples/ruvector-plugins/shared/index.ts +20 -0
  36. package/examples/ruvector-plugins/shared/vector-utils.ts +257 -0
  37. package/examples/ruvector-plugins/sona-learning.ts +445 -0
  38. package/package.json +97 -0
  39. package/src/collections/collection-manager.ts +661 -0
  40. package/src/collections/index.ts +56 -0
  41. package/src/collections/official/index.ts +1040 -0
  42. package/src/core/base-plugin.ts +416 -0
  43. package/src/core/plugin-interface.ts +215 -0
  44. package/src/hooks/index.ts +685 -0
  45. package/src/index.ts +378 -0
  46. package/src/integrations/agentic-flow.ts +743 -0
  47. package/src/integrations/index.ts +88 -0
  48. package/src/integrations/ruvector/ARCHITECTURE.md +1245 -0
  49. package/src/integrations/ruvector/attention-advanced.ts +1040 -0
  50. package/src/integrations/ruvector/attention-executor.ts +782 -0
  51. package/src/integrations/ruvector/attention-mechanisms.ts +757 -0
  52. package/src/integrations/ruvector/attention.ts +1063 -0
  53. package/src/integrations/ruvector/gnn.ts +3050 -0
  54. package/src/integrations/ruvector/hyperbolic.ts +1948 -0
  55. package/src/integrations/ruvector/index.ts +394 -0
  56. package/src/integrations/ruvector/migrations/001_create_extension.sql +135 -0
  57. package/src/integrations/ruvector/migrations/002_create_vector_tables.sql +259 -0
  58. package/src/integrations/ruvector/migrations/003_create_indices.sql +328 -0
  59. package/src/integrations/ruvector/migrations/004_create_functions.sql +598 -0
  60. package/src/integrations/ruvector/migrations/005_create_attention_functions.sql +654 -0
  61. package/src/integrations/ruvector/migrations/006_create_gnn_functions.sql +728 -0
  62. package/src/integrations/ruvector/migrations/007_create_hyperbolic_functions.sql +762 -0
  63. package/src/integrations/ruvector/migrations/index.ts +35 -0
  64. package/src/integrations/ruvector/migrations/migrations.ts +647 -0
  65. package/src/integrations/ruvector/quantization.ts +2036 -0
  66. package/src/integrations/ruvector/ruvector-bridge.ts +2000 -0
  67. package/src/integrations/ruvector/self-learning.ts +2376 -0
  68. package/src/integrations/ruvector/streaming.ts +1737 -0
  69. package/src/integrations/ruvector/types.ts +1945 -0
  70. package/src/providers/index.ts +643 -0
  71. package/src/registry/dependency-graph.ts +568 -0
  72. package/src/registry/enhanced-plugin-registry.ts +994 -0
  73. package/src/registry/plugin-registry.ts +604 -0
  74. package/src/sdk/index.ts +563 -0
  75. package/src/security/index.ts +594 -0
  76. package/src/types/index.ts +446 -0
  77. package/src/workers/index.ts +700 -0
  78. package/tmp.json +0 -0
  79. package/tsconfig.json +25 -0
  80. package/vitest.config.ts +23 -0
@@ -0,0 +1,2429 @@
1
+ /**
2
+ * RuVector PostgreSQL Bridge Tests
3
+ *
4
+ * Comprehensive test suite for the RuVector PostgreSQL vector database
5
+ * integration, covering connection management, vector operations,
6
+ * attention mechanisms, GNN layers, hyperbolic embeddings, and self-learning.
7
+ *
8
+ * @module @sparkleideas/plugins/__tests__/ruvector-bridge
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
12
+ import type {
13
+ RuVectorConfig,
14
+ RuVectorClientOptions,
15
+ IRuVectorClient,
16
+ IRuVectorTransaction,
17
+ VectorSearchOptions,
18
+ VectorSearchResult,
19
+ VectorInsertOptions,
20
+ VectorUpdateOptions,
21
+ VectorIndexOptions,
22
+ BatchVectorOptions,
23
+ AttentionConfig,
24
+ AttentionInput,
25
+ AttentionOutput,
26
+ AttentionMechanism,
27
+ GNNLayer,
28
+ GraphData,
29
+ GNNOutput,
30
+ GNNLayerType,
31
+ HyperbolicEmbedding,
32
+ HyperbolicInput,
33
+ HyperbolicOutput,
34
+ HyperbolicModel,
35
+ DistanceMetric,
36
+ VectorIndexType,
37
+ HealthStatus,
38
+ IndexStats,
39
+ QueryResult,
40
+ BatchResult,
41
+ BulkSearchResult,
42
+ ConnectionResult,
43
+ PoolConfig,
44
+ SSLConfig,
45
+ RetryConfig,
46
+ RuVectorEventType,
47
+ RuVectorEvent,
48
+ RuVectorStats,
49
+ AnalysisResult,
50
+ } from '../src/integrations/ruvector/types.js';
51
+ import {
52
+ isDistanceMetric,
53
+ isAttentionMechanism,
54
+ isGNNLayerType,
55
+ isHyperbolicModel,
56
+ isVectorIndexType,
57
+ isSuccess,
58
+ isError,
59
+ } from '../src/integrations/ruvector/types.js';
60
+
61
+ // ============================================================================
62
+ // Mock Types and Utilities
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Mock PostgreSQL client interface
67
+ */
68
+ interface MockPgClient {
69
+ connect: Mock;
70
+ query: Mock;
71
+ release: Mock;
72
+ end: Mock;
73
+ on: Mock;
74
+ off: Mock;
75
+ }
76
+
77
+ /**
78
+ * Mock PostgreSQL pool interface
79
+ */
80
+ interface MockPgPool {
81
+ connect: Mock;
82
+ query: Mock;
83
+ end: Mock;
84
+ on: Mock;
85
+ totalCount: number;
86
+ idleCount: number;
87
+ waitingCount: number;
88
+ }
89
+
90
+ /**
91
+ * Create a mock PostgreSQL client
92
+ */
93
+ function createMockPgClient(): MockPgClient {
94
+ return {
95
+ connect: vi.fn().mockResolvedValue(undefined),
96
+ query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
97
+ release: vi.fn(),
98
+ end: vi.fn().mockResolvedValue(undefined),
99
+ on: vi.fn(),
100
+ off: vi.fn(),
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Create a mock PostgreSQL pool
106
+ */
107
+ function createMockPgPool(): MockPgPool {
108
+ const mockClient = createMockPgClient();
109
+ return {
110
+ connect: vi.fn().mockResolvedValue(mockClient),
111
+ query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
112
+ end: vi.fn().mockResolvedValue(undefined),
113
+ on: vi.fn(),
114
+ totalCount: 10,
115
+ idleCount: 5,
116
+ waitingCount: 0,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Create test configuration
122
+ */
123
+ function createTestConfig(overrides: Partial<RuVectorConfig> = {}): RuVectorConfig {
124
+ return {
125
+ host: 'localhost',
126
+ port: 5432,
127
+ database: 'test_db',
128
+ user: 'test_user',
129
+ password: 'test_password',
130
+ poolSize: 10,
131
+ connectionTimeoutMs: 5000,
132
+ queryTimeoutMs: 30000,
133
+ ...overrides,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Create test vector
139
+ */
140
+ function createTestVector(dimensions: number = 384): number[] {
141
+ return Array.from({ length: dimensions }, () => Math.random() * 2 - 1);
142
+ }
143
+
144
+ /**
145
+ * Normalize a vector for cosine similarity
146
+ */
147
+ function normalizeVector(vector: number[]): number[] {
148
+ const magnitude = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
149
+ return vector.map((v) => v / magnitude);
150
+ }
151
+
152
+ // ============================================================================
153
+ // Mock RuVector Client Implementation for Testing
154
+ // ============================================================================
155
+
156
+ /**
157
+ * Mock RuVector client for unit testing
158
+ */
159
+ class MockRuVectorClient implements IRuVectorClient {
160
+ private pool: MockPgPool;
161
+ private config: RuVectorClientOptions;
162
+ private connected: boolean = false;
163
+ private eventHandlers: Map<RuVectorEventType, Set<(event: RuVectorEvent) => void>> = new Map();
164
+ private connectionInfo: ConnectionResult | null = null;
165
+
166
+ constructor(config: RuVectorClientOptions) {
167
+ this.config = config;
168
+ this.pool = createMockPgPool();
169
+ }
170
+
171
+ // Event Emitter Implementation
172
+ on<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): () => void {
173
+ if (!this.eventHandlers.has(event)) {
174
+ this.eventHandlers.set(event, new Set());
175
+ }
176
+ this.eventHandlers.get(event)!.add(handler as (e: RuVectorEvent) => void);
177
+ return () => this.off(event, handler);
178
+ }
179
+
180
+ off<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): void {
181
+ this.eventHandlers.get(event)?.delete(handler as (e: RuVectorEvent) => void);
182
+ }
183
+
184
+ once<T extends RuVectorEventType>(event: T, handler: (e: RuVectorEvent<T>) => void): () => void {
185
+ const wrappedHandler = (e: RuVectorEvent<T>) => {
186
+ this.off(event, wrappedHandler);
187
+ handler(e);
188
+ };
189
+ return this.on(event, wrappedHandler);
190
+ }
191
+
192
+ emit<T extends RuVectorEventType>(event: T, data: RuVectorEvent<T>['data']): void {
193
+ const handlers = this.eventHandlers.get(event);
194
+ if (handlers) {
195
+ handlers.forEach((handler) =>
196
+ handler({ type: event, timestamp: new Date(), data } as RuVectorEvent)
197
+ );
198
+ }
199
+ }
200
+
201
+ removeAllListeners(event?: RuVectorEventType): void {
202
+ if (event) {
203
+ this.eventHandlers.delete(event);
204
+ } else {
205
+ this.eventHandlers.clear();
206
+ }
207
+ }
208
+
209
+ // Connection Management
210
+ async connect(): Promise<ConnectionResult> {
211
+ this.connected = true;
212
+ this.connectionInfo = {
213
+ connectionId: 'test-connection-1',
214
+ ready: true,
215
+ serverVersion: '15.0',
216
+ ruVectorVersion: '1.0.0',
217
+ parameters: {
218
+ server_encoding: 'UTF8',
219
+ client_encoding: 'UTF8',
220
+ },
221
+ };
222
+ this.emit('connection:open', {
223
+ connectionId: this.connectionInfo.connectionId,
224
+ host: this.config.host,
225
+ port: this.config.port,
226
+ database: this.config.database,
227
+ });
228
+ return this.connectionInfo;
229
+ }
230
+
231
+ async disconnect(): Promise<void> {
232
+ this.connected = false;
233
+ this.emit('connection:close', {
234
+ connectionId: this.connectionInfo?.connectionId || '',
235
+ host: this.config.host,
236
+ port: this.config.port,
237
+ database: this.config.database,
238
+ });
239
+ this.connectionInfo = null;
240
+ }
241
+
242
+ isConnected(): boolean {
243
+ return this.connected;
244
+ }
245
+
246
+ getConnectionInfo(): ConnectionResult | null {
247
+ return this.connectionInfo;
248
+ }
249
+
250
+ // Vector Operations
251
+ async search(options: VectorSearchOptions): Promise<VectorSearchResult[]> {
252
+ if (!this.connected) throw new Error('Not connected');
253
+
254
+ this.emit('search:start', {
255
+ searchId: 'search-1',
256
+ tableName: options.tableName || 'vectors',
257
+ k: options.k,
258
+ metric: options.metric,
259
+ hasFilters: !!options.filter,
260
+ });
261
+
262
+ const results: VectorSearchResult[] = Array.from({ length: Math.min(options.k, 10) }, (_, i) => ({
263
+ id: `result-${i}`,
264
+ score: 1 - i * 0.1,
265
+ distance: i * 0.1,
266
+ metadata: options.includeMetadata ? { index: i } : undefined,
267
+ vector: options.includeVector ? createTestVector(options.query.length) : undefined,
268
+ rank: i + 1,
269
+ }));
270
+
271
+ this.emit('search:complete', {
272
+ searchId: 'search-1',
273
+ durationMs: 15,
274
+ resultCount: results.length,
275
+ scannedCount: 1000,
276
+ cacheHit: false,
277
+ });
278
+
279
+ return results;
280
+ }
281
+
282
+ async batchSearch(options: BatchVectorOptions): Promise<BulkSearchResult> {
283
+ if (!this.connected) throw new Error('Not connected');
284
+
285
+ const results: VectorSearchResult[][] = await Promise.all(
286
+ options.queries.map((query) =>
287
+ this.search({
288
+ query: Array.from(query),
289
+ k: options.k,
290
+ metric: options.metric,
291
+ filter: options.filter,
292
+ tableName: options.tableName,
293
+ vectorColumn: options.vectorColumn,
294
+ })
295
+ )
296
+ );
297
+
298
+ return {
299
+ results,
300
+ totalDurationMs: results.length * 15,
301
+ avgDurationMs: 15,
302
+ cacheStats: {
303
+ hits: 0,
304
+ misses: results.length,
305
+ hitRate: 0,
306
+ },
307
+ };
308
+ }
309
+
310
+ async insert(options: VectorInsertOptions): Promise<BatchResult<string>> {
311
+ if (!this.connected) throw new Error('Not connected');
312
+
313
+ const ids: string[] = options.vectors.map(
314
+ (v, i) => v.id?.toString() || `generated-${i}`
315
+ );
316
+
317
+ options.vectors.forEach((v, i) => {
318
+ this.emit('vector:inserted', {
319
+ tableName: options.tableName,
320
+ vectorId: ids[i],
321
+ dimensions: Array.isArray(v.vector) ? v.vector.length : v.vector.length,
322
+ });
323
+ });
324
+
325
+ return {
326
+ total: options.vectors.length,
327
+ successful: options.vectors.length,
328
+ failed: 0,
329
+ results: ids,
330
+ durationMs: options.vectors.length * 5,
331
+ throughput: options.vectors.length / ((options.vectors.length * 5) / 1000),
332
+ };
333
+ }
334
+
335
+ async update(options: VectorUpdateOptions): Promise<boolean> {
336
+ if (!this.connected) throw new Error('Not connected');
337
+
338
+ this.emit('vector:updated', {
339
+ tableName: options.tableName,
340
+ vectorId: options.id,
341
+ dimensions: options.vector ? (Array.isArray(options.vector) ? options.vector.length : options.vector.length) : 0,
342
+ });
343
+
344
+ return true;
345
+ }
346
+
347
+ async delete(tableName: string, id: string | number): Promise<boolean> {
348
+ if (!this.connected) throw new Error('Not connected');
349
+
350
+ this.emit('vector:deleted', {
351
+ tableName,
352
+ vectorId: id,
353
+ dimensions: 0,
354
+ });
355
+
356
+ return true;
357
+ }
358
+
359
+ async bulkDelete(tableName: string, ids: Array<string | number>): Promise<BatchResult> {
360
+ if (!this.connected) throw new Error('Not connected');
361
+
362
+ return {
363
+ total: ids.length,
364
+ successful: ids.length,
365
+ failed: 0,
366
+ durationMs: ids.length * 2,
367
+ throughput: ids.length / ((ids.length * 2) / 1000),
368
+ };
369
+ }
370
+
371
+ // Index Management
372
+ async createIndex(options: VectorIndexOptions): Promise<void> {
373
+ if (!this.connected) throw new Error('Not connected');
374
+
375
+ this.emit('index:created', {
376
+ indexName: options.indexName || `idx_${options.tableName}_${options.columnName}`,
377
+ tableName: options.tableName,
378
+ columnName: options.columnName,
379
+ indexType: options.indexType,
380
+ durationMs: 1000,
381
+ });
382
+ }
383
+
384
+ async dropIndex(indexName: string): Promise<void> {
385
+ if (!this.connected) throw new Error('Not connected');
386
+
387
+ this.emit('index:dropped', {
388
+ indexName,
389
+ tableName: '',
390
+ columnName: '',
391
+ indexType: 'hnsw',
392
+ });
393
+ }
394
+
395
+ async rebuildIndex(indexName: string): Promise<void> {
396
+ if (!this.connected) throw new Error('Not connected');
397
+
398
+ this.emit('index:rebuilt', {
399
+ indexName,
400
+ tableName: '',
401
+ columnName: '',
402
+ indexType: 'hnsw',
403
+ durationMs: 2000,
404
+ });
405
+ }
406
+
407
+ async getIndexStats(indexName: string): Promise<IndexStats> {
408
+ if (!this.connected) throw new Error('Not connected');
409
+
410
+ return {
411
+ indexName,
412
+ indexType: 'hnsw',
413
+ numVectors: 10000,
414
+ sizeBytes: 1024 * 1024 * 50, // 50MB
415
+ buildTimeMs: 5000,
416
+ lastRebuild: new Date(),
417
+ params: {
418
+ m: 16,
419
+ efConstruction: 200,
420
+ },
421
+ };
422
+ }
423
+
424
+ async listIndices(tableName?: string): Promise<IndexStats[]> {
425
+ if (!this.connected) throw new Error('Not connected');
426
+
427
+ return [
428
+ {
429
+ indexName: `idx_${tableName || 'vectors'}_embedding`,
430
+ indexType: 'hnsw',
431
+ numVectors: 10000,
432
+ sizeBytes: 1024 * 1024 * 50,
433
+ buildTimeMs: 5000,
434
+ lastRebuild: new Date(),
435
+ params: { m: 16, efConstruction: 200 },
436
+ },
437
+ ];
438
+ }
439
+
440
+ // Attention Operations
441
+ async computeAttention(input: AttentionInput, config: AttentionConfig): Promise<AttentionOutput> {
442
+ if (!this.connected) throw new Error('Not connected');
443
+
444
+ const seqLen = input.query.length;
445
+ const outputDim = config.numHeads * config.headDim;
446
+
447
+ const output: number[][] = input.query.map(() =>
448
+ Array.from({ length: outputDim }, () => Math.random() * 2 - 1)
449
+ );
450
+
451
+ this.emit('attention:computed', {
452
+ mechanism: config.mechanism,
453
+ seqLen,
454
+ numHeads: config.numHeads,
455
+ durationMs: seqLen * 0.1,
456
+ memoryBytes: seqLen * outputDim * 4,
457
+ });
458
+
459
+ return {
460
+ output,
461
+ attentionWeights: config.params?.checkpointing
462
+ ? undefined
463
+ : input.query.map(() =>
464
+ Array.from({ length: config.numHeads }, () =>
465
+ Array.from({ length: seqLen }, () =>
466
+ Array.from({ length: seqLen }, () => Math.random())
467
+ )
468
+ )
469
+ ),
470
+ stats: {
471
+ computeTimeMs: seqLen * 0.1,
472
+ memoryBytes: seqLen * outputDim * 4,
473
+ tokensProcessed: seqLen,
474
+ },
475
+ };
476
+ }
477
+
478
+ // GNN Operations
479
+ async runGNNLayer(graph: GraphData, layer: GNNLayer): Promise<GNNOutput> {
480
+ if (!this.connected) throw new Error('Not connected');
481
+
482
+ const numNodes = graph.nodeFeatures.length;
483
+ const numEdges = graph.edgeIndex[0].length;
484
+
485
+ const nodeEmbeddings: number[][] = graph.nodeFeatures.map(() =>
486
+ Array.from({ length: layer.outputDim }, () => Math.random() * 2 - 1)
487
+ );
488
+
489
+ this.emit('gnn:forward', {
490
+ layerType: layer.type,
491
+ numNodes,
492
+ numEdges,
493
+ durationMs: numNodes * 0.05,
494
+ });
495
+
496
+ return {
497
+ nodeEmbeddings,
498
+ graphEmbedding: graph.batch
499
+ ? Array.from({ length: layer.outputDim }, () => Math.random())
500
+ : undefined,
501
+ attentionWeights:
502
+ layer.type === 'gat' || layer.type === 'gat_v2'
503
+ ? graph.edgeIndex[0].map(() => Math.random())
504
+ : undefined,
505
+ stats: {
506
+ forwardTimeMs: numNodes * 0.05,
507
+ numNodes,
508
+ numEdges,
509
+ memoryBytes: numNodes * layer.outputDim * 4,
510
+ numIterations: 1,
511
+ },
512
+ };
513
+ }
514
+
515
+ buildGraph(nodeFeatures: number[][], edges: [number, number][]): GraphData {
516
+ return {
517
+ nodeFeatures,
518
+ edgeIndex: [edges.map((e) => e[0]), edges.map((e) => e[1])],
519
+ };
520
+ }
521
+
522
+ // Hyperbolic Operations
523
+ async hyperbolicEmbed(
524
+ input: HyperbolicInput,
525
+ config: HyperbolicEmbedding
526
+ ): Promise<HyperbolicOutput> {
527
+ if (!this.connected) throw new Error('Not connected');
528
+
529
+ const embeddings: number[][] = input.points.map((point) => {
530
+ // Project to Poincare ball (ensure ||x|| < 1)
531
+ const norm = Math.sqrt(point.reduce((sum, v) => sum + v * v, 0));
532
+ const scale = norm >= 1 ? 0.99 / norm : 1;
533
+ return point.map((v) => v * scale);
534
+ });
535
+
536
+ this.emit('hyperbolic:embed', {
537
+ model: config.model,
538
+ operation: 'embed',
539
+ numPoints: input.points.length,
540
+ durationMs: input.points.length * 0.02,
541
+ });
542
+
543
+ return {
544
+ embeddings,
545
+ curvature: config.curvature,
546
+ };
547
+ }
548
+
549
+ async hyperbolicDistance(
550
+ a: number[],
551
+ b: number[],
552
+ config: HyperbolicEmbedding
553
+ ): Promise<number> {
554
+ if (!this.connected) throw new Error('Not connected');
555
+
556
+ // Poincare distance formula: arccosh(1 + 2 * ||u-v||^2 / ((1-||u||^2)(1-||v||^2)))
557
+ const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
558
+ const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
559
+ const diffNorm = Math.sqrt(a.reduce((sum, v, i) => sum + (v - b[i]) ** 2, 0));
560
+
561
+ const numerator = 2 * diffNorm ** 2;
562
+ const denominator = (1 - normA ** 2) * (1 - normB ** 2);
563
+ const distance = Math.acosh(1 + numerator / denominator);
564
+
565
+ this.emit('hyperbolic:distance', {
566
+ model: config.model,
567
+ operation: 'distance',
568
+ numPoints: 2,
569
+ durationMs: 0.01,
570
+ });
571
+
572
+ return distance;
573
+ }
574
+
575
+ // Embedding Operations
576
+ async embed(text: string, model?: string): Promise<{ embedding: number[]; model: string; tokenCount: number; durationMs: number; dimension: number }> {
577
+ if (!this.connected) throw new Error('Not connected');
578
+
579
+ const dimension = 384;
580
+ return {
581
+ embedding: createTestVector(dimension),
582
+ model: model || 'default',
583
+ tokenCount: Math.ceil(text.length / 4),
584
+ durationMs: 50,
585
+ dimension,
586
+ };
587
+ }
588
+
589
+ async embedBatch(texts: string[], model?: string): Promise<{ embeddings: Array<{ embedding: number[]; model: string; tokenCount: number; durationMs: number; dimension: number }>; totalTokens: number; totalDurationMs: number; throughput: number }> {
590
+ if (!this.connected) throw new Error('Not connected');
591
+
592
+ const embeddings = await Promise.all(texts.map((t) => this.embed(t, model)));
593
+ const totalTokens = embeddings.reduce((sum, e) => sum + e.tokenCount, 0);
594
+ const totalDurationMs = embeddings.reduce((sum, e) => sum + e.durationMs, 0);
595
+
596
+ return {
597
+ embeddings,
598
+ totalTokens,
599
+ totalDurationMs,
600
+ throughput: totalTokens / (totalDurationMs / 1000),
601
+ };
602
+ }
603
+
604
+ // Transaction Support
605
+ async transaction<T>(fn: (tx: IRuVectorTransaction) => Promise<T>): Promise<{ transactionId: string; committed: boolean; data?: T; durationMs: number; queryCount: number }> {
606
+ if (!this.connected) throw new Error('Not connected');
607
+
608
+ const startTime = Date.now();
609
+ let queryCount = 0;
610
+
611
+ const tx: IRuVectorTransaction = {
612
+ query: async <R = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<R>> => {
613
+ queryCount++;
614
+ return { rows: [] as R[], rowCount: 0, durationMs: 5, command: 'SELECT' };
615
+ },
616
+ insert: async (options: VectorInsertOptions) => {
617
+ queryCount++;
618
+ return this.insert(options);
619
+ },
620
+ update: async (options: VectorUpdateOptions) => {
621
+ queryCount++;
622
+ return this.update(options);
623
+ },
624
+ delete: async (tableName: string, id: string | number) => {
625
+ queryCount++;
626
+ return this.delete(tableName, id);
627
+ },
628
+ commit: async () => {},
629
+ rollback: async () => {},
630
+ };
631
+
632
+ try {
633
+ const data = await fn(tx);
634
+ await tx.commit();
635
+ return {
636
+ transactionId: 'tx-1',
637
+ committed: true,
638
+ data,
639
+ durationMs: Date.now() - startTime,
640
+ queryCount,
641
+ };
642
+ } catch (error) {
643
+ await tx.rollback();
644
+ throw error;
645
+ }
646
+ }
647
+
648
+ // Admin Operations
649
+ async vacuum(tableName?: string): Promise<void> {
650
+ if (!this.connected) throw new Error('Not connected');
651
+
652
+ this.emit('admin:vacuum', {
653
+ operation: 'vacuum',
654
+ tableName,
655
+ durationMs: 500,
656
+ });
657
+ }
658
+
659
+ async analyze(tableName?: string): Promise<AnalysisResult> {
660
+ if (!this.connected) throw new Error('Not connected');
661
+
662
+ this.emit('admin:analyze', {
663
+ operation: 'analyze',
664
+ tableName,
665
+ durationMs: 300,
666
+ });
667
+
668
+ return {
669
+ tableName: tableName || 'all',
670
+ numRows: 10000,
671
+ columnStats: [
672
+ {
673
+ columnName: 'embedding',
674
+ dataType: 'vector(384)',
675
+ nullPercent: 0,
676
+ distinctCount: 10000,
677
+ avgSizeBytes: 1536,
678
+ },
679
+ ],
680
+ recommendations: ['Consider adding an HNSW index for faster similarity search'],
681
+ };
682
+ }
683
+
684
+ async healthCheck(): Promise<HealthStatus> {
685
+ return {
686
+ status: this.connected ? 'healthy' : 'unhealthy',
687
+ components: {
688
+ database: {
689
+ name: 'PostgreSQL',
690
+ healthy: this.connected,
691
+ latencyMs: this.connected ? 5 : undefined,
692
+ error: this.connected ? undefined : 'Not connected',
693
+ },
694
+ ruvector: {
695
+ name: 'RuVector Extension',
696
+ healthy: this.connected,
697
+ latencyMs: this.connected ? 1 : undefined,
698
+ },
699
+ pool: {
700
+ name: 'Connection Pool',
701
+ healthy: true,
702
+ latencyMs: 0,
703
+ },
704
+ },
705
+ lastCheck: new Date(),
706
+ issues: this.connected ? [] : ['Database connection is not established'],
707
+ };
708
+ }
709
+
710
+ async getStats(): Promise<RuVectorStats> {
711
+ if (!this.connected) throw new Error('Not connected');
712
+
713
+ return {
714
+ version: '1.0.0',
715
+ totalVectors: 100000,
716
+ totalSizeBytes: 1024 * 1024 * 500, // 500MB
717
+ numIndices: 3,
718
+ numTables: 5,
719
+ queryStats: {
720
+ totalQueries: 50000,
721
+ avgQueryTimeMs: 15,
722
+ p95QueryTimeMs: 50,
723
+ p99QueryTimeMs: 100,
724
+ cacheHitRate: 0.85,
725
+ },
726
+ memoryStats: {
727
+ usedBytes: 1024 * 1024 * 256, // 256MB
728
+ peakBytes: 1024 * 1024 * 512, // 512MB
729
+ indexBytes: 1024 * 1024 * 150, // 150MB
730
+ cacheBytes: 1024 * 1024 * 50, // 50MB
731
+ },
732
+ };
733
+ }
734
+
735
+ // Test helpers
736
+ getPool(): MockPgPool {
737
+ return this.pool;
738
+ }
739
+
740
+ getConfig(): RuVectorClientOptions {
741
+ return this.config;
742
+ }
743
+ }
744
+
745
+ // ============================================================================
746
+ // Test Suites
747
+ // ============================================================================
748
+
749
+ describe('RuVectorBridge', () => {
750
+ let client: MockRuVectorClient;
751
+ let config: RuVectorClientOptions;
752
+
753
+ beforeEach(() => {
754
+ config = {
755
+ ...createTestConfig(),
756
+ autoReconnect: true,
757
+ maxReconnectAttempts: 3,
758
+ };
759
+ client = new MockRuVectorClient(config);
760
+ });
761
+
762
+ afterEach(async () => {
763
+ if (client.isConnected()) {
764
+ await client.disconnect();
765
+ }
766
+ });
767
+
768
+ // ==========================================================================
769
+ // Connection Management Tests
770
+ // ==========================================================================
771
+
772
+ describe('Connection Management', () => {
773
+ describe('Pool Creation and Configuration', () => {
774
+ it('should create connection pool with default settings', async () => {
775
+ await client.connect();
776
+
777
+ expect(client.isConnected()).toBe(true);
778
+ expect(client.getConnectionInfo()).not.toBeNull();
779
+ });
780
+
781
+ it('should apply custom pool configuration', async () => {
782
+ const customConfig: RuVectorClientOptions = {
783
+ ...config,
784
+ pool: {
785
+ min: 5,
786
+ max: 20,
787
+ idleTimeoutMs: 30000,
788
+ acquireTimeoutMs: 10000,
789
+ validateOnAcquire: true,
790
+ },
791
+ };
792
+
793
+ const customClient = new MockRuVectorClient(customConfig);
794
+ await customClient.connect();
795
+
796
+ expect(customClient.isConnected()).toBe(true);
797
+ expect(customClient.getConfig().pool?.min).toBe(5);
798
+ expect(customClient.getConfig().pool?.max).toBe(20);
799
+
800
+ await customClient.disconnect();
801
+ });
802
+
803
+ it('should respect pool size limits', async () => {
804
+ const poolConfig: PoolConfig = {
805
+ min: 2,
806
+ max: 5,
807
+ idleTimeoutMs: 10000,
808
+ acquireTimeoutMs: 5000,
809
+ };
810
+
811
+ const limitedClient = new MockRuVectorClient({
812
+ ...config,
813
+ pool: poolConfig,
814
+ });
815
+
816
+ await limitedClient.connect();
817
+
818
+ expect(limitedClient.getConfig().pool?.max).toBe(5);
819
+
820
+ await limitedClient.disconnect();
821
+ });
822
+ });
823
+
824
+ describe('Connection Health Checks', () => {
825
+ it('should report healthy status when connected', async () => {
826
+ await client.connect();
827
+
828
+ const health = await client.healthCheck();
829
+
830
+ expect(health.status).toBe('healthy');
831
+ expect(health.components.database.healthy).toBe(true);
832
+ expect(health.components.ruvector.healthy).toBe(true);
833
+ expect(health.issues).toHaveLength(0);
834
+ });
835
+
836
+ it('should report unhealthy status when disconnected', async () => {
837
+ const health = await client.healthCheck();
838
+
839
+ expect(health.status).toBe('unhealthy');
840
+ expect(health.components.database.healthy).toBe(false);
841
+ expect(health.issues.length).toBeGreaterThan(0);
842
+ });
843
+
844
+ it('should include latency metrics in health check', async () => {
845
+ await client.connect();
846
+
847
+ const health = await client.healthCheck();
848
+
849
+ expect(health.components.database.latencyMs).toBeDefined();
850
+ expect(health.components.database.latencyMs).toBeLessThan(100);
851
+ });
852
+ });
853
+
854
+ describe('Reconnection on Failure', () => {
855
+ it('should emit connection events', async () => {
856
+ const openHandler = vi.fn();
857
+ const closeHandler = vi.fn();
858
+
859
+ client.on('connection:open', openHandler);
860
+ client.on('connection:close', closeHandler);
861
+
862
+ await client.connect();
863
+ expect(openHandler).toHaveBeenCalledTimes(1);
864
+
865
+ await client.disconnect();
866
+ expect(closeHandler).toHaveBeenCalledTimes(1);
867
+ });
868
+
869
+ it('should track connection state correctly', async () => {
870
+ expect(client.isConnected()).toBe(false);
871
+ expect(client.getConnectionInfo()).toBeNull();
872
+
873
+ await client.connect();
874
+ expect(client.isConnected()).toBe(true);
875
+ expect(client.getConnectionInfo()).not.toBeNull();
876
+
877
+ await client.disconnect();
878
+ expect(client.isConnected()).toBe(false);
879
+ expect(client.getConnectionInfo()).toBeNull();
880
+ });
881
+
882
+ it('should support auto-reconnect configuration', () => {
883
+ expect(client.getConfig().autoReconnect).toBe(true);
884
+ expect(client.getConfig().maxReconnectAttempts).toBe(3);
885
+ });
886
+ });
887
+
888
+ describe('SSL/TLS Connections', () => {
889
+ it('should accept boolean SSL configuration', async () => {
890
+ const sslClient = new MockRuVectorClient({
891
+ ...config,
892
+ ssl: true,
893
+ });
894
+
895
+ await sslClient.connect();
896
+ expect(sslClient.isConnected()).toBe(true);
897
+ expect(sslClient.getConfig().ssl).toBe(true);
898
+
899
+ await sslClient.disconnect();
900
+ });
901
+
902
+ it('should accept detailed SSL configuration', async () => {
903
+ const sslConfig: SSLConfig = {
904
+ enabled: true,
905
+ rejectUnauthorized: true,
906
+ ca: '/path/to/ca.pem',
907
+ cert: '/path/to/cert.pem',
908
+ key: '/path/to/key.pem',
909
+ servername: 'db.example.com',
910
+ };
911
+
912
+ const sslClient = new MockRuVectorClient({
913
+ ...config,
914
+ ssl: sslConfig,
915
+ });
916
+
917
+ await sslClient.connect();
918
+ expect(sslClient.isConnected()).toBe(true);
919
+
920
+ const clientSsl = sslClient.getConfig().ssl as SSLConfig;
921
+ expect(clientSsl.enabled).toBe(true);
922
+ expect(clientSsl.rejectUnauthorized).toBe(true);
923
+ expect(clientSsl.ca).toBe('/path/to/ca.pem');
924
+
925
+ await sslClient.disconnect();
926
+ });
927
+ });
928
+
929
+ describe('Retry Configuration', () => {
930
+ it('should accept retry configuration', () => {
931
+ const retryConfig: RetryConfig = {
932
+ maxAttempts: 5,
933
+ initialDelayMs: 100,
934
+ maxDelayMs: 5000,
935
+ backoffMultiplier: 2,
936
+ jitter: true,
937
+ retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT'],
938
+ };
939
+
940
+ const retryClient = new MockRuVectorClient({
941
+ ...config,
942
+ retry: retryConfig,
943
+ });
944
+
945
+ expect(retryClient.getConfig().retry?.maxAttempts).toBe(5);
946
+ expect(retryClient.getConfig().retry?.backoffMultiplier).toBe(2);
947
+ });
948
+ });
949
+ });
950
+
951
+ // ==========================================================================
952
+ // Vector Operations Tests
953
+ // ==========================================================================
954
+
955
+ describe('Vector Operations', () => {
956
+ beforeEach(async () => {
957
+ await client.connect();
958
+ });
959
+
960
+ describe('Vector Search', () => {
961
+ it('should perform cosine similarity search', async () => {
962
+ const options: VectorSearchOptions = {
963
+ query: createTestVector(384),
964
+ k: 10,
965
+ metric: 'cosine',
966
+ tableName: 'embeddings',
967
+ };
968
+
969
+ const results = await client.search(options);
970
+
971
+ expect(results).toHaveLength(10);
972
+ expect(results[0].score).toBeGreaterThan(results[1].score);
973
+ results.forEach((r) => {
974
+ expect(r.id).toBeDefined();
975
+ expect(r.score).toBeDefined();
976
+ });
977
+ });
978
+
979
+ it('should perform euclidean distance search', async () => {
980
+ const options: VectorSearchOptions = {
981
+ query: createTestVector(384),
982
+ k: 5,
983
+ metric: 'euclidean',
984
+ tableName: 'embeddings',
985
+ };
986
+
987
+ const results = await client.search(options);
988
+
989
+ expect(results).toHaveLength(5);
990
+ results.forEach((r) => {
991
+ expect(r.distance).toBeDefined();
992
+ });
993
+ });
994
+
995
+ it('should perform dot product search', async () => {
996
+ const options: VectorSearchOptions = {
997
+ query: createTestVector(384),
998
+ k: 5,
999
+ metric: 'dot',
1000
+ tableName: 'embeddings',
1001
+ };
1002
+
1003
+ const results = await client.search(options);
1004
+
1005
+ expect(results).toHaveLength(5);
1006
+ });
1007
+
1008
+ it('should include metadata when requested', async () => {
1009
+ const options: VectorSearchOptions = {
1010
+ query: createTestVector(384),
1011
+ k: 3,
1012
+ metric: 'cosine',
1013
+ includeMetadata: true,
1014
+ };
1015
+
1016
+ const results = await client.search(options);
1017
+
1018
+ results.forEach((r) => {
1019
+ expect(r.metadata).toBeDefined();
1020
+ });
1021
+ });
1022
+
1023
+ it('should include vectors when requested', async () => {
1024
+ const options: VectorSearchOptions = {
1025
+ query: createTestVector(384),
1026
+ k: 3,
1027
+ metric: 'cosine',
1028
+ includeVector: true,
1029
+ };
1030
+
1031
+ const results = await client.search(options);
1032
+
1033
+ results.forEach((r) => {
1034
+ expect(r.vector).toBeDefined();
1035
+ expect(r.vector).toHaveLength(384);
1036
+ });
1037
+ });
1038
+
1039
+ it('should emit search events', async () => {
1040
+ const startHandler = vi.fn();
1041
+ const completeHandler = vi.fn();
1042
+
1043
+ client.on('search:start', startHandler);
1044
+ client.on('search:complete', completeHandler);
1045
+
1046
+ await client.search({
1047
+ query: createTestVector(384),
1048
+ k: 5,
1049
+ metric: 'cosine',
1050
+ });
1051
+
1052
+ expect(startHandler).toHaveBeenCalled();
1053
+ expect(completeHandler).toHaveBeenCalled();
1054
+ });
1055
+
1056
+ it('should throw when not connected', async () => {
1057
+ await client.disconnect();
1058
+
1059
+ await expect(
1060
+ client.search({
1061
+ query: createTestVector(384),
1062
+ k: 5,
1063
+ metric: 'cosine',
1064
+ })
1065
+ ).rejects.toThrow('Not connected');
1066
+ });
1067
+ });
1068
+
1069
+ describe('Vector Insert/Update/Delete', () => {
1070
+ it('should insert single vector', async () => {
1071
+ const options: VectorInsertOptions = {
1072
+ tableName: 'embeddings',
1073
+ vectors: [
1074
+ {
1075
+ id: 'test-1',
1076
+ vector: createTestVector(384),
1077
+ metadata: { label: 'test' },
1078
+ },
1079
+ ],
1080
+ };
1081
+
1082
+ const result = await client.insert(options);
1083
+
1084
+ expect(result.successful).toBe(1);
1085
+ expect(result.failed).toBe(0);
1086
+ expect(result.results).toContain('test-1');
1087
+ });
1088
+
1089
+ it('should insert multiple vectors in batch', async () => {
1090
+ const options: VectorInsertOptions = {
1091
+ tableName: 'embeddings',
1092
+ vectors: Array.from({ length: 100 }, (_, i) => ({
1093
+ id: `batch-${i}`,
1094
+ vector: createTestVector(384),
1095
+ metadata: { index: i },
1096
+ })),
1097
+ batchSize: 50,
1098
+ };
1099
+
1100
+ const result = await client.insert(options);
1101
+
1102
+ expect(result.total).toBe(100);
1103
+ expect(result.successful).toBe(100);
1104
+ expect(result.throughput).toBeGreaterThan(0);
1105
+ });
1106
+
1107
+ it('should update existing vector', async () => {
1108
+ const options: VectorUpdateOptions = {
1109
+ tableName: 'embeddings',
1110
+ id: 'test-1',
1111
+ vector: createTestVector(384),
1112
+ metadata: { updated: true },
1113
+ };
1114
+
1115
+ const success = await client.update(options);
1116
+
1117
+ expect(success).toBe(true);
1118
+ });
1119
+
1120
+ it('should delete vector by id', async () => {
1121
+ const success = await client.delete('embeddings', 'test-1');
1122
+
1123
+ expect(success).toBe(true);
1124
+ });
1125
+
1126
+ it('should bulk delete vectors', async () => {
1127
+ const result = await client.bulkDelete('embeddings', ['id-1', 'id-2', 'id-3']);
1128
+
1129
+ expect(result.total).toBe(3);
1130
+ expect(result.successful).toBe(3);
1131
+ });
1132
+
1133
+ it('should emit vector events', async () => {
1134
+ const insertHandler = vi.fn();
1135
+ const updateHandler = vi.fn();
1136
+ const deleteHandler = vi.fn();
1137
+
1138
+ client.on('vector:inserted', insertHandler);
1139
+ client.on('vector:updated', updateHandler);
1140
+ client.on('vector:deleted', deleteHandler);
1141
+
1142
+ await client.insert({
1143
+ tableName: 'test',
1144
+ vectors: [{ vector: createTestVector(384) }],
1145
+ });
1146
+ await client.update({
1147
+ tableName: 'test',
1148
+ id: '1',
1149
+ vector: createTestVector(384),
1150
+ });
1151
+ await client.delete('test', '1');
1152
+
1153
+ expect(insertHandler).toHaveBeenCalled();
1154
+ expect(updateHandler).toHaveBeenCalled();
1155
+ expect(deleteHandler).toHaveBeenCalled();
1156
+ });
1157
+ });
1158
+
1159
+ describe('Batch Operations', () => {
1160
+ it('should perform batch search', async () => {
1161
+ const options: BatchVectorOptions = {
1162
+ queries: Array.from({ length: 5 }, () => createTestVector(384)),
1163
+ k: 3,
1164
+ metric: 'cosine',
1165
+ tableName: 'embeddings',
1166
+ };
1167
+
1168
+ const result = await client.batchSearch(options);
1169
+
1170
+ expect(result.results).toHaveLength(5);
1171
+ result.results.forEach((r) => {
1172
+ expect(r).toHaveLength(3);
1173
+ });
1174
+ expect(result.cacheStats).toBeDefined();
1175
+ });
1176
+
1177
+ it('should calculate batch statistics', async () => {
1178
+ const options: BatchVectorOptions = {
1179
+ queries: Array.from({ length: 10 }, () => createTestVector(384)),
1180
+ k: 5,
1181
+ metric: 'cosine',
1182
+ };
1183
+
1184
+ const result = await client.batchSearch(options);
1185
+
1186
+ expect(result.totalDurationMs).toBeGreaterThan(0);
1187
+ expect(result.avgDurationMs).toBeGreaterThan(0);
1188
+ expect(result.cacheStats.hitRate).toBeDefined();
1189
+ });
1190
+ });
1191
+
1192
+ describe('Index Creation', () => {
1193
+ it('should create HNSW index', async () => {
1194
+ const options: VectorIndexOptions = {
1195
+ tableName: 'embeddings',
1196
+ columnName: 'embedding',
1197
+ indexType: 'hnsw',
1198
+ m: 16,
1199
+ efConstruction: 200,
1200
+ };
1201
+
1202
+ await expect(client.createIndex(options)).resolves.toBeUndefined();
1203
+ });
1204
+
1205
+ it('should create IVFFlat index', async () => {
1206
+ const options: VectorIndexOptions = {
1207
+ tableName: 'embeddings',
1208
+ columnName: 'embedding',
1209
+ indexType: 'ivfflat',
1210
+ lists: 100,
1211
+ };
1212
+
1213
+ await expect(client.createIndex(options)).resolves.toBeUndefined();
1214
+ });
1215
+
1216
+ it('should emit index events', async () => {
1217
+ const createdHandler = vi.fn();
1218
+ client.on('index:created', createdHandler);
1219
+
1220
+ await client.createIndex({
1221
+ tableName: 'test',
1222
+ columnName: 'embedding',
1223
+ indexType: 'hnsw',
1224
+ });
1225
+
1226
+ expect(createdHandler).toHaveBeenCalled();
1227
+ });
1228
+
1229
+ it('should get index statistics', async () => {
1230
+ const stats = await client.getIndexStats('idx_embeddings_embedding');
1231
+
1232
+ expect(stats.indexName).toBe('idx_embeddings_embedding');
1233
+ expect(stats.indexType).toBeDefined();
1234
+ expect(stats.numVectors).toBeGreaterThan(0);
1235
+ expect(stats.sizeBytes).toBeGreaterThan(0);
1236
+ });
1237
+
1238
+ it('should list indices for table', async () => {
1239
+ const indices = await client.listIndices('embeddings');
1240
+
1241
+ expect(indices.length).toBeGreaterThan(0);
1242
+ indices.forEach((idx) => {
1243
+ expect(idx.indexName).toBeDefined();
1244
+ expect(idx.indexType).toBeDefined();
1245
+ });
1246
+ });
1247
+
1248
+ it('should rebuild index', async () => {
1249
+ const rebuiltHandler = vi.fn();
1250
+ client.on('index:rebuilt', rebuiltHandler);
1251
+
1252
+ await client.rebuildIndex('idx_test');
1253
+
1254
+ expect(rebuiltHandler).toHaveBeenCalled();
1255
+ });
1256
+
1257
+ it('should drop index', async () => {
1258
+ const droppedHandler = vi.fn();
1259
+ client.on('index:dropped', droppedHandler);
1260
+
1261
+ await client.dropIndex('idx_test');
1262
+
1263
+ expect(droppedHandler).toHaveBeenCalled();
1264
+ });
1265
+ });
1266
+ });
1267
+
1268
+ // ==========================================================================
1269
+ // Attention Mechanisms Tests
1270
+ // ==========================================================================
1271
+
1272
+ describe('Attention Mechanisms', () => {
1273
+ beforeEach(async () => {
1274
+ await client.connect();
1275
+ });
1276
+
1277
+ describe('Multi-Head Attention', () => {
1278
+ it('should execute multi-head attention', async () => {
1279
+ const seqLen = 10;
1280
+ const dim = 64;
1281
+
1282
+ const input: AttentionInput = {
1283
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1284
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1285
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1286
+ };
1287
+
1288
+ const config: AttentionConfig = {
1289
+ mechanism: 'multi_head',
1290
+ numHeads: 8,
1291
+ headDim: 64,
1292
+ embedDim: 512,
1293
+ };
1294
+
1295
+ const result = await client.computeAttention(input, config);
1296
+
1297
+ expect(result.output).toHaveLength(seqLen);
1298
+ expect(result.output[0]).toHaveLength(config.numHeads * config.headDim);
1299
+ expect(result.stats?.computeTimeMs).toBeDefined();
1300
+ });
1301
+
1302
+ it('should return attention weights', async () => {
1303
+ const seqLen = 5;
1304
+ const dim = 32;
1305
+
1306
+ const input: AttentionInput = {
1307
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1308
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1309
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1310
+ };
1311
+
1312
+ const config: AttentionConfig = {
1313
+ mechanism: 'multi_head',
1314
+ numHeads: 4,
1315
+ headDim: 32,
1316
+ };
1317
+
1318
+ const result = await client.computeAttention(input, config);
1319
+
1320
+ expect(result.attentionWeights).toBeDefined();
1321
+ expect(result.attentionWeights).toHaveLength(seqLen);
1322
+ });
1323
+ });
1324
+
1325
+ describe('Flash Attention Performance', () => {
1326
+ it('should execute flash attention', async () => {
1327
+ const seqLen = 100;
1328
+ const dim = 64;
1329
+
1330
+ const input: AttentionInput = {
1331
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1332
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1333
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1334
+ };
1335
+
1336
+ const config: AttentionConfig = {
1337
+ mechanism: 'flash_attention',
1338
+ numHeads: 8,
1339
+ headDim: 64,
1340
+ params: {
1341
+ flashBlockSize: 64,
1342
+ checkpointing: true,
1343
+ },
1344
+ };
1345
+
1346
+ const result = await client.computeAttention(input, config);
1347
+
1348
+ expect(result.output).toHaveLength(seqLen);
1349
+ expect(result.stats?.memoryBytes).toBeDefined();
1350
+ });
1351
+
1352
+ it('should not return weights with checkpointing enabled', async () => {
1353
+ const seqLen = 50;
1354
+ const dim = 32;
1355
+
1356
+ const input: AttentionInput = {
1357
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1358
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1359
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1360
+ };
1361
+
1362
+ const config: AttentionConfig = {
1363
+ mechanism: 'flash_attention',
1364
+ numHeads: 4,
1365
+ headDim: 32,
1366
+ params: {
1367
+ checkpointing: true,
1368
+ },
1369
+ };
1370
+
1371
+ const result = await client.computeAttention(input, config);
1372
+
1373
+ expect(result.attentionWeights).toBeUndefined();
1374
+ });
1375
+ });
1376
+
1377
+ describe('Sparse Attention Patterns', () => {
1378
+ it('should execute sparse attention', async () => {
1379
+ const seqLen = 20;
1380
+ const dim = 32;
1381
+
1382
+ const input: AttentionInput = {
1383
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1384
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1385
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1386
+ };
1387
+
1388
+ const config: AttentionConfig = {
1389
+ mechanism: 'sparse_attention',
1390
+ numHeads: 4,
1391
+ headDim: 32,
1392
+ params: {
1393
+ blockSize: 4,
1394
+ numGlobalTokens: 2,
1395
+ },
1396
+ };
1397
+
1398
+ const result = await client.computeAttention(input, config);
1399
+
1400
+ expect(result.output).toHaveLength(seqLen);
1401
+ });
1402
+
1403
+ it('should execute sliding window attention', async () => {
1404
+ const seqLen = 30;
1405
+ const dim = 32;
1406
+
1407
+ const input: AttentionInput = {
1408
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1409
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1410
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1411
+ };
1412
+
1413
+ const config: AttentionConfig = {
1414
+ mechanism: 'sliding_window',
1415
+ numHeads: 4,
1416
+ headDim: 32,
1417
+ params: {
1418
+ windowSize: 8,
1419
+ },
1420
+ };
1421
+
1422
+ const result = await client.computeAttention(input, config);
1423
+
1424
+ expect(result.output).toHaveLength(seqLen);
1425
+ });
1426
+ });
1427
+
1428
+ describe('Cross-Attention Operations', () => {
1429
+ it('should execute cross-attention between two sequences', async () => {
1430
+ const queryLen = 10;
1431
+ const keyLen = 20;
1432
+ const dim = 32;
1433
+
1434
+ const input: AttentionInput = {
1435
+ query: Array.from({ length: queryLen }, () => createTestVector(dim)),
1436
+ key: Array.from({ length: keyLen }, () => createTestVector(dim)),
1437
+ value: Array.from({ length: keyLen }, () => createTestVector(dim)),
1438
+ };
1439
+
1440
+ const config: AttentionConfig = {
1441
+ mechanism: 'cross_attention',
1442
+ numHeads: 4,
1443
+ headDim: 32,
1444
+ };
1445
+
1446
+ const result = await client.computeAttention(input, config);
1447
+
1448
+ expect(result.output).toHaveLength(queryLen);
1449
+ });
1450
+ });
1451
+
1452
+ describe('Attention Events', () => {
1453
+ it('should emit attention computed event', async () => {
1454
+ const handler = vi.fn();
1455
+ client.on('attention:computed', handler);
1456
+
1457
+ const seqLen = 5;
1458
+ const dim = 32;
1459
+
1460
+ const input: AttentionInput = {
1461
+ query: Array.from({ length: seqLen }, () => createTestVector(dim)),
1462
+ key: Array.from({ length: seqLen }, () => createTestVector(dim)),
1463
+ value: Array.from({ length: seqLen }, () => createTestVector(dim)),
1464
+ };
1465
+
1466
+ await client.computeAttention(input, {
1467
+ mechanism: 'multi_head',
1468
+ numHeads: 2,
1469
+ headDim: 32,
1470
+ });
1471
+
1472
+ expect(handler).toHaveBeenCalled();
1473
+ const eventData = handler.mock.calls[0][0].data;
1474
+ expect(eventData.mechanism).toBe('multi_head');
1475
+ expect(eventData.seqLen).toBe(seqLen);
1476
+ expect(eventData.numHeads).toBe(2);
1477
+ });
1478
+ });
1479
+ });
1480
+
1481
+ // ==========================================================================
1482
+ // GNN Layers Tests
1483
+ // ==========================================================================
1484
+
1485
+ describe('GNN Layers', () => {
1486
+ beforeEach(async () => {
1487
+ await client.connect();
1488
+ });
1489
+
1490
+ function createTestGraph(numNodes: number, numEdges: number, featureDim: number): GraphData {
1491
+ const nodeFeatures = Array.from({ length: numNodes }, () => createTestVector(featureDim));
1492
+ const edges: [number, number][] = Array.from({ length: numEdges }, () => [
1493
+ Math.floor(Math.random() * numNodes),
1494
+ Math.floor(Math.random() * numNodes),
1495
+ ]);
1496
+ return client.buildGraph(nodeFeatures, edges);
1497
+ }
1498
+
1499
+ describe('GCN Forward Pass', () => {
1500
+ it('should execute GCN layer', async () => {
1501
+ const graph = createTestGraph(50, 100, 64);
1502
+
1503
+ const layer: GNNLayer = {
1504
+ type: 'gcn',
1505
+ inputDim: 64,
1506
+ outputDim: 32,
1507
+ normalize: true,
1508
+ addSelfLoops: true,
1509
+ activation: 'relu',
1510
+ };
1511
+
1512
+ const result = await client.runGNNLayer(graph, layer);
1513
+
1514
+ expect(result.nodeEmbeddings).toHaveLength(50);
1515
+ expect(result.nodeEmbeddings[0]).toHaveLength(32);
1516
+ expect(result.stats?.numNodes).toBe(50);
1517
+ expect(result.stats?.numEdges).toBe(100);
1518
+ });
1519
+
1520
+ it('should support different aggregations', async () => {
1521
+ const graph = createTestGraph(20, 40, 32);
1522
+
1523
+ const layer: GNNLayer = {
1524
+ type: 'gcn',
1525
+ inputDim: 32,
1526
+ outputDim: 16,
1527
+ aggregation: 'sum',
1528
+ };
1529
+
1530
+ const result = await client.runGNNLayer(graph, layer);
1531
+
1532
+ expect(result.nodeEmbeddings).toHaveLength(20);
1533
+ });
1534
+ });
1535
+
1536
+ describe('GAT Attention Weights', () => {
1537
+ it('should execute GAT layer with attention', async () => {
1538
+ const graph = createTestGraph(30, 60, 64);
1539
+
1540
+ const layer: GNNLayer = {
1541
+ type: 'gat',
1542
+ inputDim: 64,
1543
+ outputDim: 32,
1544
+ numHeads: 4,
1545
+ dropout: 0.1,
1546
+ params: {
1547
+ negativeSlope: 0.2,
1548
+ concat: true,
1549
+ },
1550
+ };
1551
+
1552
+ const result = await client.runGNNLayer(graph, layer);
1553
+
1554
+ expect(result.nodeEmbeddings).toHaveLength(30);
1555
+ expect(result.attentionWeights).toBeDefined();
1556
+ expect(result.attentionWeights).toHaveLength(60);
1557
+ });
1558
+
1559
+ it('should execute GAT v2 layer', async () => {
1560
+ const graph = createTestGraph(25, 50, 32);
1561
+
1562
+ const layer: GNNLayer = {
1563
+ type: 'gat_v2',
1564
+ inputDim: 32,
1565
+ outputDim: 16,
1566
+ numHeads: 2,
1567
+ };
1568
+
1569
+ const result = await client.runGNNLayer(graph, layer);
1570
+
1571
+ expect(result.nodeEmbeddings).toHaveLength(25);
1572
+ expect(result.attentionWeights).toBeDefined();
1573
+ });
1574
+ });
1575
+
1576
+ describe('GraphSAGE Sampling', () => {
1577
+ it('should execute GraphSAGE layer', async () => {
1578
+ const graph = createTestGraph(100, 500, 64);
1579
+
1580
+ const layer: GNNLayer = {
1581
+ type: 'sage',
1582
+ inputDim: 64,
1583
+ outputDim: 32,
1584
+ aggregation: 'mean',
1585
+ params: {
1586
+ sampleSize: 10,
1587
+ samplingStrategy: 'uniform',
1588
+ },
1589
+ };
1590
+
1591
+ const result = await client.runGNNLayer(graph, layer);
1592
+
1593
+ expect(result.nodeEmbeddings).toHaveLength(100);
1594
+ expect(result.nodeEmbeddings[0]).toHaveLength(32);
1595
+ });
1596
+
1597
+ it('should support importance sampling', async () => {
1598
+ const graph = createTestGraph(50, 200, 32);
1599
+
1600
+ const layer: GNNLayer = {
1601
+ type: 'sage',
1602
+ inputDim: 32,
1603
+ outputDim: 16,
1604
+ aggregation: 'max',
1605
+ params: {
1606
+ sampleSize: 5,
1607
+ samplingStrategy: 'importance',
1608
+ },
1609
+ };
1610
+
1611
+ const result = await client.runGNNLayer(graph, layer);
1612
+
1613
+ expect(result.nodeEmbeddings).toHaveLength(50);
1614
+ });
1615
+ });
1616
+
1617
+ describe('Message Passing', () => {
1618
+ it('should execute MPNN layer', async () => {
1619
+ const graph = createTestGraph(40, 80, 48);
1620
+
1621
+ const layer: GNNLayer = {
1622
+ type: 'mpnn',
1623
+ inputDim: 48,
1624
+ outputDim: 24,
1625
+ hiddenDim: 64,
1626
+ aggregation: 'attention',
1627
+ };
1628
+
1629
+ const result = await client.runGNNLayer(graph, layer);
1630
+
1631
+ expect(result.nodeEmbeddings).toHaveLength(40);
1632
+ expect(result.stats?.numIterations).toBeGreaterThan(0);
1633
+ });
1634
+
1635
+ it('should emit GNN events', async () => {
1636
+ const forwardHandler = vi.fn();
1637
+ client.on('gnn:forward', forwardHandler);
1638
+
1639
+ const graph = createTestGraph(10, 20, 16);
1640
+
1641
+ await client.runGNNLayer(graph, {
1642
+ type: 'gcn',
1643
+ inputDim: 16,
1644
+ outputDim: 8,
1645
+ });
1646
+
1647
+ expect(forwardHandler).toHaveBeenCalled();
1648
+ const eventData = forwardHandler.mock.calls[0][0].data;
1649
+ expect(eventData.layerType).toBe('gcn');
1650
+ expect(eventData.numNodes).toBe(10);
1651
+ expect(eventData.numEdges).toBe(20);
1652
+ });
1653
+ });
1654
+
1655
+ describe('Graph Building', () => {
1656
+ it('should build graph from node features and edges', () => {
1657
+ const nodeFeatures = [
1658
+ [1, 2, 3],
1659
+ [4, 5, 6],
1660
+ [7, 8, 9],
1661
+ ];
1662
+ const edges: [number, number][] = [
1663
+ [0, 1],
1664
+ [1, 2],
1665
+ [2, 0],
1666
+ ];
1667
+
1668
+ const graph = client.buildGraph(nodeFeatures, edges);
1669
+
1670
+ expect(graph.nodeFeatures).toEqual(nodeFeatures);
1671
+ expect(graph.edgeIndex[0]).toEqual([0, 1, 2]);
1672
+ expect(graph.edgeIndex[1]).toEqual([1, 2, 0]);
1673
+ });
1674
+ });
1675
+ });
1676
+
1677
+ // ==========================================================================
1678
+ // Hyperbolic Embeddings Tests
1679
+ // ==========================================================================
1680
+
1681
+ describe('Hyperbolic Embeddings', () => {
1682
+ beforeEach(async () => {
1683
+ await client.connect();
1684
+ });
1685
+
1686
+ describe('Poincare Distance Calculations', () => {
1687
+ it('should compute Poincare distance', async () => {
1688
+ const config: HyperbolicEmbedding = {
1689
+ model: 'poincare',
1690
+ curvature: -1,
1691
+ dimension: 32,
1692
+ };
1693
+
1694
+ // Points inside Poincare ball (||x|| < 1)
1695
+ const a = Array.from({ length: 32 }, () => Math.random() * 0.5);
1696
+ const b = Array.from({ length: 32 }, () => Math.random() * 0.5);
1697
+
1698
+ const distance = await client.hyperbolicDistance(a, b, config);
1699
+
1700
+ expect(distance).toBeGreaterThanOrEqual(0);
1701
+ expect(Number.isFinite(distance)).toBe(true);
1702
+ });
1703
+
1704
+ it('should return zero distance for same point', async () => {
1705
+ const config: HyperbolicEmbedding = {
1706
+ model: 'poincare',
1707
+ curvature: -1,
1708
+ dimension: 16,
1709
+ };
1710
+
1711
+ const point = Array.from({ length: 16 }, () => Math.random() * 0.3);
1712
+
1713
+ const distance = await client.hyperbolicDistance(point, point, config);
1714
+
1715
+ expect(distance).toBeCloseTo(0, 5);
1716
+ });
1717
+
1718
+ it('should emit hyperbolic distance event', async () => {
1719
+ const handler = vi.fn();
1720
+ client.on('hyperbolic:distance', handler);
1721
+
1722
+ const config: HyperbolicEmbedding = {
1723
+ model: 'poincare',
1724
+ curvature: -1,
1725
+ dimension: 8,
1726
+ };
1727
+
1728
+ await client.hyperbolicDistance(
1729
+ [0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2],
1730
+ [0.3, 0.1, 0.3, 0.1, 0.3, 0.1, 0.3, 0.1],
1731
+ config
1732
+ );
1733
+
1734
+ expect(handler).toHaveBeenCalled();
1735
+ expect(handler.mock.calls[0][0].data.operation).toBe('distance');
1736
+ });
1737
+ });
1738
+
1739
+ describe('Lorentz Operations', () => {
1740
+ it('should compute embeddings in Lorentz model', async () => {
1741
+ const config: HyperbolicEmbedding = {
1742
+ model: 'lorentz',
1743
+ curvature: -1,
1744
+ dimension: 32,
1745
+ params: {
1746
+ timeDim: 0,
1747
+ },
1748
+ };
1749
+
1750
+ const input: HyperbolicInput = {
1751
+ points: Array.from({ length: 10 }, () => createTestVector(32)),
1752
+ };
1753
+
1754
+ const result = await client.hyperbolicEmbed(input, config);
1755
+
1756
+ expect(result.embeddings).toHaveLength(10);
1757
+ expect(result.curvature).toBe(-1);
1758
+ });
1759
+ });
1760
+
1761
+ describe('Model Conversions', () => {
1762
+ it('should support Poincare model', async () => {
1763
+ const config: HyperbolicEmbedding = {
1764
+ model: 'poincare',
1765
+ curvature: -1,
1766
+ dimension: 16,
1767
+ };
1768
+
1769
+ const input: HyperbolicInput = {
1770
+ points: [[0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2]],
1771
+ };
1772
+
1773
+ const result = await client.hyperbolicEmbed(input, config);
1774
+
1775
+ expect(result.embeddings).toHaveLength(1);
1776
+ // Verify point is inside Poincare ball
1777
+ const norm = Math.sqrt(result.embeddings[0].reduce((sum, v) => sum + v * v, 0));
1778
+ expect(norm).toBeLessThan(1);
1779
+ });
1780
+
1781
+ it('should support Klein model', async () => {
1782
+ const config: HyperbolicEmbedding = {
1783
+ model: 'klein',
1784
+ curvature: -1,
1785
+ dimension: 8,
1786
+ };
1787
+
1788
+ const input: HyperbolicInput = {
1789
+ points: Array.from({ length: 5 }, () =>
1790
+ Array.from({ length: 8 }, () => Math.random() * 0.8)
1791
+ ),
1792
+ };
1793
+
1794
+ const result = await client.hyperbolicEmbed(input, config);
1795
+
1796
+ expect(result.embeddings).toHaveLength(5);
1797
+ });
1798
+ });
1799
+
1800
+ describe('Manifold Projections', () => {
1801
+ it('should project points to Poincare ball', async () => {
1802
+ const config: HyperbolicEmbedding = {
1803
+ model: 'poincare',
1804
+ curvature: -1,
1805
+ dimension: 16,
1806
+ params: {
1807
+ maxNorm: 0.99,
1808
+ eps: 1e-5,
1809
+ },
1810
+ };
1811
+
1812
+ // Points that may be outside the ball
1813
+ const input: HyperbolicInput = {
1814
+ points: Array.from({ length: 5 }, () =>
1815
+ Array.from({ length: 16 }, () => Math.random() * 2 - 1)
1816
+ ),
1817
+ };
1818
+
1819
+ const result = await client.hyperbolicEmbed(input, config);
1820
+
1821
+ // All points should be projected inside the ball
1822
+ result.embeddings.forEach((emb) => {
1823
+ const norm = Math.sqrt(emb.reduce((sum, v) => sum + v * v, 0));
1824
+ expect(norm).toBeLessThan(1);
1825
+ });
1826
+ });
1827
+
1828
+ it('should emit hyperbolic embed event', async () => {
1829
+ const handler = vi.fn();
1830
+ client.on('hyperbolic:embed', handler);
1831
+
1832
+ const config: HyperbolicEmbedding = {
1833
+ model: 'poincare',
1834
+ curvature: -1,
1835
+ dimension: 8,
1836
+ };
1837
+
1838
+ await client.hyperbolicEmbed(
1839
+ { points: [[0.1, 0.2, 0.1, 0.2, 0.1, 0.2, 0.1, 0.2]] },
1840
+ config
1841
+ );
1842
+
1843
+ expect(handler).toHaveBeenCalled();
1844
+ expect(handler.mock.calls[0][0].data.model).toBe('poincare');
1845
+ expect(handler.mock.calls[0][0].data.operation).toBe('embed');
1846
+ });
1847
+ });
1848
+ });
1849
+
1850
+ // ==========================================================================
1851
+ // Self-Learning Tests
1852
+ // ==========================================================================
1853
+
1854
+ describe('Self-Learning', () => {
1855
+ beforeEach(async () => {
1856
+ await client.connect();
1857
+ });
1858
+
1859
+ describe('Query Optimization Suggestions', () => {
1860
+ it('should analyze table and provide recommendations', async () => {
1861
+ const analysis = await client.analyze('embeddings');
1862
+
1863
+ expect(analysis.recommendations).toBeDefined();
1864
+ expect(Array.isArray(analysis.recommendations)).toBe(true);
1865
+ expect(analysis.numRows).toBeGreaterThan(0);
1866
+ });
1867
+
1868
+ it('should provide column statistics', async () => {
1869
+ const analysis = await client.analyze('test_table');
1870
+
1871
+ expect(analysis.columnStats).toBeDefined();
1872
+ expect(analysis.columnStats.length).toBeGreaterThan(0);
1873
+
1874
+ const embeddingColumn = analysis.columnStats.find(
1875
+ (c) => c.columnName === 'embedding'
1876
+ );
1877
+ expect(embeddingColumn).toBeDefined();
1878
+ expect(embeddingColumn?.dataType).toContain('vector');
1879
+ });
1880
+ });
1881
+
1882
+ describe('Index Tuning Recommendations', () => {
1883
+ it('should return index statistics for tuning', async () => {
1884
+ const stats = await client.getIndexStats('idx_embeddings_embedding');
1885
+
1886
+ expect(stats.params).toBeDefined();
1887
+ expect(stats.numVectors).toBeGreaterThan(0);
1888
+ });
1889
+
1890
+ it('should provide query statistics for optimization', async () => {
1891
+ const stats = await client.getStats();
1892
+
1893
+ expect(stats.queryStats).toBeDefined();
1894
+ expect(stats.queryStats.avgQueryTimeMs).toBeDefined();
1895
+ expect(stats.queryStats.p95QueryTimeMs).toBeDefined();
1896
+ expect(stats.queryStats.cacheHitRate).toBeDefined();
1897
+ });
1898
+ });
1899
+
1900
+ describe('Pattern Detection', () => {
1901
+ it('should track memory statistics', async () => {
1902
+ const stats = await client.getStats();
1903
+
1904
+ expect(stats.memoryStats).toBeDefined();
1905
+ expect(stats.memoryStats.usedBytes).toBeGreaterThan(0);
1906
+ expect(stats.memoryStats.indexBytes).toBeGreaterThan(0);
1907
+ expect(stats.memoryStats.cacheBytes).toBeGreaterThan(0);
1908
+ });
1909
+
1910
+ it('should emit admin events for analysis', async () => {
1911
+ const analyzeHandler = vi.fn();
1912
+ const vacuumHandler = vi.fn();
1913
+
1914
+ client.on('admin:analyze', analyzeHandler);
1915
+ client.on('admin:vacuum', vacuumHandler);
1916
+
1917
+ await client.analyze('test');
1918
+ await client.vacuum('test');
1919
+
1920
+ expect(analyzeHandler).toHaveBeenCalled();
1921
+ expect(vacuumHandler).toHaveBeenCalled();
1922
+ });
1923
+ });
1924
+ });
1925
+
1926
+ // ==========================================================================
1927
+ // Type Guard Tests
1928
+ // ==========================================================================
1929
+
1930
+ describe('Type Guards', () => {
1931
+ describe('isDistanceMetric', () => {
1932
+ it('should return true for valid distance metrics', () => {
1933
+ expect(isDistanceMetric('cosine')).toBe(true);
1934
+ expect(isDistanceMetric('euclidean')).toBe(true);
1935
+ expect(isDistanceMetric('dot')).toBe(true);
1936
+ expect(isDistanceMetric('hamming')).toBe(true);
1937
+ expect(isDistanceMetric('manhattan')).toBe(true);
1938
+ });
1939
+
1940
+ it('should return false for invalid values', () => {
1941
+ expect(isDistanceMetric('invalid')).toBe(false);
1942
+ expect(isDistanceMetric(123)).toBe(false);
1943
+ expect(isDistanceMetric(null)).toBe(false);
1944
+ expect(isDistanceMetric(undefined)).toBe(false);
1945
+ });
1946
+ });
1947
+
1948
+ describe('isAttentionMechanism', () => {
1949
+ it('should return true for valid attention mechanisms', () => {
1950
+ expect(isAttentionMechanism('multi_head')).toBe(true);
1951
+ expect(isAttentionMechanism('flash_attention')).toBe(true);
1952
+ expect(isAttentionMechanism('sparse_attention')).toBe(true);
1953
+ expect(isAttentionMechanism('cross_attention')).toBe(true);
1954
+ });
1955
+
1956
+ it('should return false for invalid values', () => {
1957
+ expect(isAttentionMechanism('invalid')).toBe(false);
1958
+ expect(isAttentionMechanism(42)).toBe(false);
1959
+ });
1960
+ });
1961
+
1962
+ describe('isGNNLayerType', () => {
1963
+ it('should return true for valid GNN layer types', () => {
1964
+ expect(isGNNLayerType('gcn')).toBe(true);
1965
+ expect(isGNNLayerType('gat')).toBe(true);
1966
+ expect(isGNNLayerType('sage')).toBe(true);
1967
+ expect(isGNNLayerType('mpnn')).toBe(true);
1968
+ });
1969
+
1970
+ it('should return false for invalid values', () => {
1971
+ expect(isGNNLayerType('invalid')).toBe(false);
1972
+ expect(isGNNLayerType([])).toBe(false);
1973
+ });
1974
+ });
1975
+
1976
+ describe('isHyperbolicModel', () => {
1977
+ it('should return true for valid hyperbolic models', () => {
1978
+ expect(isHyperbolicModel('poincare')).toBe(true);
1979
+ expect(isHyperbolicModel('lorentz')).toBe(true);
1980
+ expect(isHyperbolicModel('klein')).toBe(true);
1981
+ expect(isHyperbolicModel('half_space')).toBe(true);
1982
+ });
1983
+
1984
+ it('should return false for invalid values', () => {
1985
+ expect(isHyperbolicModel('invalid')).toBe(false);
1986
+ expect(isHyperbolicModel({})).toBe(false);
1987
+ });
1988
+ });
1989
+
1990
+ describe('isVectorIndexType', () => {
1991
+ it('should return true for valid index types', () => {
1992
+ expect(isVectorIndexType('hnsw')).toBe(true);
1993
+ expect(isVectorIndexType('ivfflat')).toBe(true);
1994
+ expect(isVectorIndexType('flat')).toBe(true);
1995
+ });
1996
+
1997
+ it('should return false for invalid values', () => {
1998
+ expect(isVectorIndexType('btree')).toBe(false);
1999
+ expect(isVectorIndexType(true)).toBe(false);
2000
+ });
2001
+ });
2002
+
2003
+ describe('Result Type Guards', () => {
2004
+ it('should identify successful results', () => {
2005
+ const success = { success: true as const, data: 'test' };
2006
+ const error = { success: false as const, error: new Error('test') };
2007
+
2008
+ expect(isSuccess(success)).toBe(true);
2009
+ expect(isSuccess(error)).toBe(false);
2010
+ });
2011
+
2012
+ it('should identify error results', () => {
2013
+ const success = { success: true as const, data: 'test' };
2014
+ const error = { success: false as const, error: new Error('test') };
2015
+
2016
+ expect(isError(error)).toBe(true);
2017
+ expect(isError(success)).toBe(false);
2018
+ });
2019
+ });
2020
+ });
2021
+
2022
+ // ==========================================================================
2023
+ // Transaction Tests
2024
+ // ==========================================================================
2025
+
2026
+ describe('Transactions', () => {
2027
+ beforeEach(async () => {
2028
+ await client.connect();
2029
+ });
2030
+
2031
+ it('should execute transaction successfully', async () => {
2032
+ const result = await client.transaction(async (tx) => {
2033
+ await tx.insert({
2034
+ tableName: 'test',
2035
+ vectors: [{ vector: createTestVector(384) }],
2036
+ });
2037
+ await tx.update({
2038
+ tableName: 'test',
2039
+ id: '1',
2040
+ vector: createTestVector(384),
2041
+ });
2042
+ return { success: true };
2043
+ });
2044
+
2045
+ expect(result.committed).toBe(true);
2046
+ expect(result.data?.success).toBe(true);
2047
+ expect(result.queryCount).toBe(2);
2048
+ });
2049
+
2050
+ it('should rollback on error', async () => {
2051
+ await expect(
2052
+ client.transaction(async (tx) => {
2053
+ await tx.insert({
2054
+ tableName: 'test',
2055
+ vectors: [{ vector: createTestVector(384) }],
2056
+ });
2057
+ throw new Error('Simulated error');
2058
+ })
2059
+ ).rejects.toThrow('Simulated error');
2060
+ });
2061
+
2062
+ it('should track transaction metrics', async () => {
2063
+ const result = await client.transaction(async (tx) => {
2064
+ for (let i = 0; i < 5; i++) {
2065
+ await tx.query('SELECT 1');
2066
+ }
2067
+ return 'done';
2068
+ });
2069
+
2070
+ expect(result.transactionId).toBeDefined();
2071
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
2072
+ expect(result.queryCount).toBe(5);
2073
+ });
2074
+ });
2075
+
2076
+ // ==========================================================================
2077
+ // Event System Tests
2078
+ // ==========================================================================
2079
+
2080
+ describe('Event System', () => {
2081
+ it('should register and unregister event handlers', async () => {
2082
+ const handler = vi.fn();
2083
+
2084
+ const unsubscribe = client.on('connection:open', handler);
2085
+
2086
+ await client.connect();
2087
+ expect(handler).toHaveBeenCalledTimes(1);
2088
+
2089
+ unsubscribe();
2090
+ await client.disconnect();
2091
+ await client.connect();
2092
+
2093
+ expect(handler).toHaveBeenCalledTimes(1); // Should not be called again
2094
+ });
2095
+
2096
+ it('should handle once events', async () => {
2097
+ const handler = vi.fn();
2098
+
2099
+ client.once('connection:open', handler);
2100
+
2101
+ await client.connect();
2102
+ await client.disconnect();
2103
+ await client.connect();
2104
+
2105
+ expect(handler).toHaveBeenCalledTimes(1);
2106
+ });
2107
+
2108
+ it('should remove all listeners', async () => {
2109
+ const handler1 = vi.fn();
2110
+ const handler2 = vi.fn();
2111
+
2112
+ client.on('connection:open', handler1);
2113
+ client.on('connection:close', handler2);
2114
+
2115
+ client.removeAllListeners();
2116
+
2117
+ await client.connect();
2118
+ await client.disconnect();
2119
+
2120
+ expect(handler1).not.toHaveBeenCalled();
2121
+ expect(handler2).not.toHaveBeenCalled();
2122
+ });
2123
+
2124
+ it('should remove listeners for specific event', async () => {
2125
+ const openHandler = vi.fn();
2126
+ const closeHandler = vi.fn();
2127
+
2128
+ client.on('connection:open', openHandler);
2129
+ client.on('connection:close', closeHandler);
2130
+
2131
+ client.removeAllListeners('connection:open');
2132
+
2133
+ await client.connect();
2134
+ await client.disconnect();
2135
+
2136
+ expect(openHandler).not.toHaveBeenCalled();
2137
+ expect(closeHandler).toHaveBeenCalled();
2138
+ });
2139
+ });
2140
+
2141
+ // ==========================================================================
2142
+ // Embedding Operations Tests
2143
+ // ==========================================================================
2144
+
2145
+ describe('Embedding Operations', () => {
2146
+ beforeEach(async () => {
2147
+ await client.connect();
2148
+ });
2149
+
2150
+ it('should embed single text', async () => {
2151
+ const result = await client.embed('Hello, world!');
2152
+
2153
+ expect(result.embedding).toBeDefined();
2154
+ expect(result.embedding).toHaveLength(384);
2155
+ expect(result.tokenCount).toBeGreaterThan(0);
2156
+ expect(result.durationMs).toBeGreaterThan(0);
2157
+ });
2158
+
2159
+ it('should embed batch of texts', async () => {
2160
+ const texts = ['First text', 'Second text', 'Third text'];
2161
+
2162
+ const result = await client.embedBatch(texts);
2163
+
2164
+ expect(result.embeddings).toHaveLength(3);
2165
+ expect(result.totalTokens).toBeGreaterThan(0);
2166
+ expect(result.throughput).toBeGreaterThan(0);
2167
+ });
2168
+
2169
+ it('should use specified model', async () => {
2170
+ const result = await client.embed('Test text', 'custom-model');
2171
+
2172
+ expect(result.model).toBe('custom-model');
2173
+ });
2174
+ });
2175
+
2176
+ // ==========================================================================
2177
+ // Admin Operations Tests
2178
+ // ==========================================================================
2179
+
2180
+ describe('Admin Operations', () => {
2181
+ beforeEach(async () => {
2182
+ await client.connect();
2183
+ });
2184
+
2185
+ it('should perform vacuum operation', async () => {
2186
+ await expect(client.vacuum('test_table')).resolves.toBeUndefined();
2187
+ });
2188
+
2189
+ it('should get comprehensive stats', async () => {
2190
+ const stats = await client.getStats();
2191
+
2192
+ expect(stats.version).toBeDefined();
2193
+ expect(stats.totalVectors).toBeGreaterThan(0);
2194
+ expect(stats.totalSizeBytes).toBeGreaterThan(0);
2195
+ expect(stats.numIndices).toBeGreaterThan(0);
2196
+ expect(stats.queryStats).toBeDefined();
2197
+ expect(stats.memoryStats).toBeDefined();
2198
+ });
2199
+
2200
+ it('should throw when not connected', async () => {
2201
+ await client.disconnect();
2202
+
2203
+ await expect(client.vacuum()).rejects.toThrow('Not connected');
2204
+ await expect(client.analyze()).rejects.toThrow('Not connected');
2205
+ await expect(client.getStats()).rejects.toThrow('Not connected');
2206
+ });
2207
+ });
2208
+ });
2209
+
2210
+ // ============================================================================
2211
+ // Edge Case Tests
2212
+ // ============================================================================
2213
+
2214
+ describe('Edge Cases', () => {
2215
+ let client: MockRuVectorClient;
2216
+
2217
+ beforeEach(async () => {
2218
+ client = new MockRuVectorClient(createTestConfig());
2219
+ await client.connect();
2220
+ });
2221
+
2222
+ afterEach(async () => {
2223
+ await client.disconnect();
2224
+ });
2225
+
2226
+ describe('Boundary Values', () => {
2227
+ it('should handle empty vector search results', async () => {
2228
+ const results = await client.search({
2229
+ query: createTestVector(384),
2230
+ k: 0,
2231
+ metric: 'cosine',
2232
+ });
2233
+
2234
+ expect(results).toHaveLength(0);
2235
+ });
2236
+
2237
+ it('should handle large batch sizes', async () => {
2238
+ const options: BatchVectorOptions = {
2239
+ queries: Array.from({ length: 100 }, () => createTestVector(384)),
2240
+ k: 10,
2241
+ metric: 'cosine',
2242
+ };
2243
+
2244
+ const result = await client.batchSearch(options);
2245
+
2246
+ expect(result.results).toHaveLength(100);
2247
+ });
2248
+
2249
+ it('should handle high-dimensional vectors', async () => {
2250
+ const highDimVector = createTestVector(4096);
2251
+
2252
+ const results = await client.search({
2253
+ query: highDimVector,
2254
+ k: 5,
2255
+ metric: 'cosine',
2256
+ });
2257
+
2258
+ expect(results.length).toBeGreaterThan(0);
2259
+ });
2260
+ });
2261
+
2262
+ describe('Empty/Null Cases', () => {
2263
+ it('should handle empty batch insert', async () => {
2264
+ const result = await client.insert({
2265
+ tableName: 'test',
2266
+ vectors: [],
2267
+ });
2268
+
2269
+ expect(result.total).toBe(0);
2270
+ expect(result.successful).toBe(0);
2271
+ });
2272
+
2273
+ it('should handle graph with no edges', async () => {
2274
+ const graph = client.buildGraph(
2275
+ [[1, 2, 3], [4, 5, 6]],
2276
+ []
2277
+ );
2278
+
2279
+ const result = await client.runGNNLayer(graph, {
2280
+ type: 'gcn',
2281
+ inputDim: 3,
2282
+ outputDim: 2,
2283
+ });
2284
+
2285
+ expect(result.nodeEmbeddings).toHaveLength(2);
2286
+ });
2287
+ });
2288
+
2289
+ describe('Concurrent Operations', () => {
2290
+ it('should handle concurrent searches', async () => {
2291
+ const searches = Array.from({ length: 10 }, () =>
2292
+ client.search({
2293
+ query: createTestVector(384),
2294
+ k: 5,
2295
+ metric: 'cosine',
2296
+ })
2297
+ );
2298
+
2299
+ const results = await Promise.all(searches);
2300
+
2301
+ expect(results).toHaveLength(10);
2302
+ results.forEach((r) => {
2303
+ expect(r).toHaveLength(5);
2304
+ });
2305
+ });
2306
+
2307
+ it('should handle concurrent inserts', async () => {
2308
+ const inserts = Array.from({ length: 5 }, (_, i) =>
2309
+ client.insert({
2310
+ tableName: 'test',
2311
+ vectors: [
2312
+ {
2313
+ id: `concurrent-${i}`,
2314
+ vector: createTestVector(384),
2315
+ },
2316
+ ],
2317
+ })
2318
+ );
2319
+
2320
+ const results = await Promise.all(inserts);
2321
+
2322
+ results.forEach((r) => {
2323
+ expect(r.successful).toBe(1);
2324
+ });
2325
+ });
2326
+ });
2327
+ });
2328
+
2329
+ // ============================================================================
2330
+ // Performance Testing
2331
+ // ============================================================================
2332
+
2333
+ describe('Performance', () => {
2334
+ let client: MockRuVectorClient;
2335
+
2336
+ beforeEach(async () => {
2337
+ client = new MockRuVectorClient(createTestConfig());
2338
+ await client.connect();
2339
+ });
2340
+
2341
+ afterEach(async () => {
2342
+ await client.disconnect();
2343
+ });
2344
+
2345
+ it('should complete single search within acceptable time', async () => {
2346
+ const start = performance.now();
2347
+
2348
+ await client.search({
2349
+ query: createTestVector(384),
2350
+ k: 10,
2351
+ metric: 'cosine',
2352
+ });
2353
+
2354
+ const duration = performance.now() - start;
2355
+
2356
+ // Mock should be fast, but in real implementation this tests latency
2357
+ expect(duration).toBeLessThan(1000);
2358
+ });
2359
+
2360
+ it('should handle batch operations efficiently', async () => {
2361
+ const batchSize = 50;
2362
+ const start = performance.now();
2363
+
2364
+ const result = await client.batchSearch({
2365
+ queries: Array.from({ length: batchSize }, () => createTestVector(384)),
2366
+ k: 5,
2367
+ metric: 'cosine',
2368
+ });
2369
+
2370
+ const duration = performance.now() - start;
2371
+ const avgPerQuery = duration / batchSize;
2372
+
2373
+ expect(result.results).toHaveLength(batchSize);
2374
+ // Average time per query should be reasonable
2375
+ expect(avgPerQuery).toBeLessThan(100);
2376
+ });
2377
+
2378
+ it('should track throughput metrics', async () => {
2379
+ const result = await client.insert({
2380
+ tableName: 'test',
2381
+ vectors: Array.from({ length: 100 }, (_, i) => ({
2382
+ id: `perf-${i}`,
2383
+ vector: createTestVector(384),
2384
+ })),
2385
+ });
2386
+
2387
+ expect(result.throughput).toBeGreaterThan(0);
2388
+ expect(result.durationMs).toBeGreaterThan(0);
2389
+ });
2390
+ });
2391
+
2392
+ // ============================================================================
2393
+ // Security Testing
2394
+ // ============================================================================
2395
+
2396
+ describe('Security', () => {
2397
+ let client: MockRuVectorClient;
2398
+
2399
+ beforeEach(async () => {
2400
+ client = new MockRuVectorClient(createTestConfig());
2401
+ await client.connect();
2402
+ });
2403
+
2404
+ afterEach(async () => {
2405
+ await client.disconnect();
2406
+ });
2407
+
2408
+ it('should not expose sensitive configuration', () => {
2409
+ const config = client.getConfig();
2410
+
2411
+ // Password should be present but we shouldn't log it
2412
+ expect(config.password).toBeDefined();
2413
+ expect(typeof config.password).toBe('string');
2414
+ });
2415
+
2416
+ it('should support SSL configuration', () => {
2417
+ const sslClient = new MockRuVectorClient({
2418
+ ...createTestConfig(),
2419
+ ssl: {
2420
+ enabled: true,
2421
+ rejectUnauthorized: true,
2422
+ },
2423
+ });
2424
+
2425
+ const sslConfig = sslClient.getConfig().ssl as SSLConfig;
2426
+ expect(sslConfig.enabled).toBe(true);
2427
+ expect(sslConfig.rejectUnauthorized).toBe(true);
2428
+ });
2429
+ });