const { test } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const binding = process.env.RUVECTOR_GNN_BINDING || path.resolve(__dirname, '../index.js');
function runNative(source) {
const child = spawnSync(process.execPath, ['-e', `
const assert = require('node:assert/strict');
const g = require(${JSON.stringify(binding)});
const q = new Float32Array([1, 0]);
const layer = new g.RuvectorLayer(2, 2, 1, 0);
${source}
`], { encoding: 'utf8', timeout: 10000 });
assert.ifError(child.error);
assert.equal(child.signal, null, child.stderr);
assert.equal(child.status, 0, child.stderr);
}
for (const length of [0, 1, 3]) {
test(`differentiableSearch rejects candidate dimension ${length} without aborting`, () => {
runNative(`assert.throws(() => g.differentiableSearch(q,
[q, new Float32Array(${length})], 1, 1), {code: 'InvalidArg'});`);
});
test(`forward rejects node dimension ${length} without aborting`, () => {
runNative(`assert.throws(() => layer.forward(new Float32Array(${length}), [],
new Float32Array()), {code: 'InvalidArg'});`);
});
test(`forward rejects neighbor dimension ${length} without aborting`, () => {
runNative(`assert.throws(() => layer.forward(q, [new Float32Array(${length})],
new Float32Array([1])), {code: 'InvalidArg'});`);
});
test(`hierarchicalForward rejects candidate dimension ${length} without aborting`, () => {
runNative(`assert.throws(() => g.hierarchicalForward(q,
[[q, new Float32Array(${length})]], [layer.toJson()]), {code: 'InvalidArg'});`);
});
}
test('hierarchicalForward checks query against the layer input dimension', () => {
runNative(`const wide = new g.RuvectorLayer(3, 2, 1, 0);
assert.throws(() => g.hierarchicalForward(q, [[q]], [wide.toJson()]),
{code: 'InvalidArg'});`);
});
test('constructor rejects zero attention heads without aborting', () => {
runNative(`assert.throws(() => new g.RuvectorLayer(2, 2, 0, 0),
{code: 'InvalidArg'});`);
});
test('hierarchicalForward checks dimensions after an earlier projection', () => {
runNative(`const wide = new g.RuvectorLayer(2, 4, 1, 0);
assert.throws(() => g.hierarchicalForward(q, [[q], [q]],
[wide.toJson(), layer.toJson()]), {code: 'InvalidArg'});`);
});
test('valid search and projected/serialized layers still return finite outputs', () => {
runNative(`assert.equal(g.differentiableSearch(q, [q, new Float32Array([0,1])],
1, 1).indices[0], 0);
const wide = new g.RuvectorLayer(2, 4, 1, 0);
const restored = g.RuvectorLayer.fromJson(wide.toJson());
for (const output of [wide.forward(q, [], new Float32Array()),
restored.forward(q, [q], new Float32Array([1])),
g.hierarchicalForward(q, [[q]], [wide.toJson()])]) {
assert.equal(output.length, 4);
assert.ok(Array.from(output).every(Number.isFinite));
}
const next = new g.RuvectorLayer(4, 2, 1, 0);
const output = g.hierarchicalForward(q, [[q], [new Float32Array([1,0,0,0])]],
[wide.toJson(), next.toJson()]);
assert.equal(output.length, 2);
assert.ok(Array.from(output).every(Number.isFinite));
assert.equal(g.differentiableSearch(q, [], 1, 1).indices.length, 0);
assert.deepEqual(Array.from(g.hierarchicalForward(q, [], [])), Array.from(q));`);
});
test('hierarchicalForward preserves empty and unmatched layer behavior', () => {
runNative(`assert.deepEqual(Array.from(g.hierarchicalForward(q, [[],[q]],
[layer.toJson()])), Array.from(q));
assert.deepEqual(Array.from(g.hierarchicalForward(q, [], [layer.toJson()])), Array.from(q));
assert.deepEqual(Array.from(g.hierarchicalForward(q, [[q]], [])), Array.from(q));`);
});
for (const [input, hidden, heads] of [[2, 0, 1], [0, 0, 1], [2, 2, 0], [2, 3, 2], [4294967295, 4294967295, 1]]) {
test(`constructor rejects invalid shape ${input}/${hidden}/${heads} before allocation`, () => {
runNative(`assert.throws(() => new g.RuvectorLayer(${input}, ${hidden}, ${heads}, 0),
{code: 'InvalidArg'});`);
});
}
const invalidModels = {
'normalization gamma': 'j.norm.gamma = {v:1, dim:[3], data:[1,1,1]}',
'normalization beta': 'j.norm.beta = {v:1, dim:[1], data:[0]}',
'normalization epsilon': 'j.norm.eps = 0',
'attention head count': 'j.attention.num_heads = 0',
'attention head size': 'j.attention.head_dim = 3',
'dropout': 'j.dropout = 2',
'message bias': 'j.w_msg.bias = {v:1, dim:[1], data:[0]}',
};
for (const field of ['w_agg', 'w_update.w_z', 'w_update.u_z', 'w_update.w_r',
'w_update.u_r', 'w_update.w_h', 'w_update.u_h', 'attention.q_linear',
'attention.k_linear', 'attention.v_linear', 'attention.out_linear']) {
invalidModels[field] = `j.${field}.weights = {v:1, dim:[1,2], data:[0,0]}`;
}
for (const [name, mutation] of Object.entries(invalidModels)) {
test(`deserialization rejects inconsistent ${name} without aborting`, () => {
runNative(`const j = JSON.parse(layer.toJson()); ${mutation};
const json = JSON.stringify(j);
assert.throws(() => g.RuvectorLayer.fromJson(json), {code: 'InvalidArg'});
assert.throws(() => g.hierarchicalForward(q, [[q]], [json]), {code: 'InvalidArg'});`);
});
}
test('malformed JSON is an invalid argument through both deserialization APIs', () => {
runNative(`assert.throws(() => g.RuvectorLayer.fromJson('{'), {code:'InvalidArg'});
assert.throws(() => g.hierarchicalForward(q, [[q]], ['{']), {code:'InvalidArg'});`);
});
test('zero-input positive-hidden layers preserve constant projection behavior', () => {
runNative(`const zero = new g.RuvectorLayer(0, 2, 1, 0);
const restored = g.RuvectorLayer.fromJson(zero.toJson());
const empty = new Float32Array();
for (const output of [zero.forward(empty, [], empty),
restored.forward(empty, [empty], new Float32Array([1])),
g.hierarchicalForward(empty, [[empty]], [zero.toJson()])]) {
assert.equal(output.length, 2);
assert.ok(Array.from(output).every(Number.isFinite));
}`);
});
test('compressWithLevel rejects zero or out-of-range PQ subvectors without aborting', () => {
runNative(`const c = new g.TensorCompress(); const e = new Float32Array(8).fill(1);
for (const levelType of ['pq8', 'pq4']) {
assert.throws(() => c.compressWithLevel(e, {levelType, subvectors: 0}));
assert.throws(() => c.compressWithLevel(e, {levelType, subvectors: 264}),
{code: 'InvalidArg'});
const ok = c.compressWithLevel(e, {levelType, subvectors: 4});
assert.equal(c.decompress(ok).length, 8);
}
assert.throws(() => c.compressWithLevel(e, {levelType: 'pq8', centroids: 256}),
{code: 'InvalidArg'});`);
});
test('decompress rejects malformed PQ JSON without aborting or huge allocation', () => {
runNative(`const c = new g.TensorCompress();
for (const pq of [
{PQ8: {codes: [0], codebooks: [[1]], subvector_dim: 0, dim: 0}},
{PQ8: {codes: [0], codebooks: [[]], subvector_dim: 2 ** 40, dim: 2 ** 40}},
{PQ4: {codes: [0, 0], codebooks: [[1]], outliers: [], subvector_dim: 1, dim: 2}},
]) assert.throws(() => c.decompress(JSON.stringify(pq)));`);
});