ruvector-gnn-node 2.0.6

Node.js bindings for Ruvector GNN via NAPI-RS
Documentation
// Basic tests for Ruvector GNN Node.js bindings

const { test } = require('node:test');
const assert = require('node:assert');

const {
  RuvectorLayer,
  TensorCompress,
  differentiableSearch,
  hierarchicalForward,
  getCompressionLevel,
  init
} = require('../index.js');

test('initialization', () => {
  const result = init();
  assert.strictEqual(typeof result, 'string');
  assert.ok(result.includes('initialized'));
});

test('RuvectorLayer creation', () => {
  const layer = new RuvectorLayer(4, 8, 2, 0.1);
  assert.ok(layer instanceof RuvectorLayer);
});

test('RuvectorLayer forward pass', () => {
  const layer = new RuvectorLayer(4, 8, 2, 0.1);
  const node = new Float32Array([1.0, 2.0, 3.0, 4.0]);
  const neighbors = [new Float32Array([0.5, 1.0, 1.5, 2.0]), new Float32Array([2.0, 3.0, 4.0, 5.0])];
  const weights = new Float32Array([0.3, 0.7]);

  const output = layer.forward(node, neighbors, weights);
  assert.strictEqual(output.length, 8);
  assert.ok(output instanceof Float32Array);
});

test('RuvectorLayer forward with no neighbors', () => {
  const layer = new RuvectorLayer(4, 8, 2, 0.1);
  const node = new Float32Array([1.0, 2.0, 3.0, 4.0]);
  const neighbors = [];
  const weights = new Float32Array([]);

  const output = layer.forward(node, neighbors, weights);
  assert.strictEqual(output.length, 8);
});

test('RuvectorLayer serialization', () => {
  const layer = new RuvectorLayer(4, 8, 2, 0.1);
  const json = layer.toJson();
  assert.strictEqual(typeof json, 'string');
  assert.ok(json.length > 0);
});

test('RuvectorLayer deserialization', () => {
  const layer1 = new RuvectorLayer(4, 8, 2, 0.1);
  const json = layer1.toJson();
  const layer2 = RuvectorLayer.fromJson(json);

  assert.ok(layer2 instanceof RuvectorLayer);

  // Test that they produce same output
  const node = new Float32Array([1.0, 2.0, 3.0, 4.0]);
  const neighbors = [new Float32Array([0.5, 1.0, 1.5, 2.0])];
  const weights = new Float32Array([1.0]);

  const output1 = layer1.forward(node, neighbors, weights);
  const output2 = layer2.forward(node, neighbors, weights);

  assert.strictEqual(output1.length, output2.length);
  for (let i = 0; i < output1.length; i++) {
    assert.ok(Math.abs(output1[i] - output2[i]) < 1e-6);
  }
});

test('TensorCompress creation', () => {
  const compressor = new TensorCompress();
  assert.ok(compressor instanceof TensorCompress);
});

test('TensorCompress adaptive compression', () => {
  const compressor = new TensorCompress();
  const embedding = new Float32Array([1.0, 2.0, 3.0, 4.0]);

  const compressed = compressor.compress(embedding, 0.5);
  assert.strictEqual(typeof compressed, 'string');
  assert.ok(compressed.length > 0);
});

test('TensorCompress round-trip', () => {
  const compressor = new TensorCompress();
  const embedding = new Float32Array([1.0, 2.0, 3.0, 4.0]);

  const compressed = compressor.compress(embedding, 1.0); // No compression
  const decompressed = compressor.decompress(compressed);

  assert.strictEqual(decompressed.length, embedding.length);
  assert.ok(decompressed instanceof Float32Array);
  for (let i = 0; i < decompressed.length; i++) {
    assert.ok(Math.abs(decompressed[i] - embedding[i]) < 1e-6);
  }
});

test('TensorCompress with explicit level', () => {
  const compressor = new TensorCompress();
  const embedding = new Float32Array(Array.from({ length: 64 }, (_, i) => i * 0.1));

  const level = {
    level_type: 'half',
    scale: 1.0
  };

  const compressed = compressor.compressWithLevel(embedding, level);
  const decompressed = compressor.decompress(compressed);

  assert.strictEqual(decompressed.length, embedding.length);
});

test('getCompressionLevel', () => {
  assert.strictEqual(getCompressionLevel(0.9), 'none');
  assert.strictEqual(getCompressionLevel(0.5), 'half');
  assert.strictEqual(getCompressionLevel(0.2), 'pq8');
  assert.strictEqual(getCompressionLevel(0.05), 'pq4');
  assert.strictEqual(getCompressionLevel(0.001), 'binary');
});

test('differentiableSearch', () => {
  const query = new Float32Array([1.0, 0.0, 0.0]);
  const candidates = [
    new Float32Array([1.0, 0.0, 0.0]),
    new Float32Array([0.9, 0.1, 0.0]),
    new Float32Array([0.0, 1.0, 0.0]),
  ];

  const result = differentiableSearch(query, candidates, 2, 1.0);

  assert.ok(Array.isArray(result.indices));
  assert.ok(Array.isArray(result.weights));
  assert.strictEqual(result.indices.length, 2);
  assert.strictEqual(result.weights.length, 2);

  // First result should be perfect match
  assert.strictEqual(result.indices[0], 0);

  // Weights should be valid probabilities
  result.weights.forEach(w => {
    assert.ok(w >= 0 && w <= 1);
  });
});

test('differentiableSearch with empty candidates', () => {
  const query = new Float32Array([1.0, 0.0, 0.0]);
  const candidates = [];

  const result = differentiableSearch(query, candidates, 2, 1.0);

  assert.strictEqual(result.indices.length, 0);
  assert.strictEqual(result.weights.length, 0);
});

test('hierarchicalForward', () => {
  const query = new Float32Array([1.0, 0.0]);
  const layerEmbeddings = [
    [new Float32Array([1.0, 0.0]), new Float32Array([0.0, 1.0])],
  ];

  const layer = new RuvectorLayer(2, 2, 1, 0.0);
  const layers = [layer.toJson()];

  const result = hierarchicalForward(query, layerEmbeddings, layers);

  assert.ok(result instanceof Float32Array);
  assert.strictEqual(result.length, 2);
});

test('invalid dropout rate throws error', () => {
  assert.throws(() => {
    new RuvectorLayer(4, 8, 2, 1.5); // dropout > 1.0
  });

  assert.throws(() => {
    new RuvectorLayer(4, 8, 2, -0.1); // dropout < 0.0
  });
});

test('compression with empty embedding throws error', () => {
  const compressor = new TensorCompress();
  assert.throws(() => {
    compressor.compress(new Float32Array([]), 0.5);
  });
});

test('compression levels produce different sizes', () => {
  const compressor = new TensorCompress();
  const embedding = new Float32Array(Array.from({ length: 64 }, (_, i) => Math.sin(i * 0.1)));

  const none = compressor.compress(embedding, 1.0);    // No compression
  const half = compressor.compress(embedding, 0.5);    // Half precision
  const binary = compressor.compress(embedding, 0.001); // Binary

  // Binary should be smallest
  assert.ok(binary.length < half.length);
  // None should be largest (or close to half)
  assert.ok(none.length >= half.length * 0.8);
});