beamdb 0.17.0

BEAM — distributed graph database syncing over WebSocket, WebRTC, and multicast. Successor to rod.
Documentation
/**
 * Layer 2: Node.js Mirror Tests for BEAM Gun.js Wire Compatibility.
 *
 * Loads the same JSON fixtures from `../wire/fixtures/`, performs equivalent
 * Gun.js operations, and reports any divergences from BEAM's parser behaviour.
 *
 * Run:  npm test   (after `npm ci`)
 *
 * Design:
 *   - Uses Node.js built-in `node:test` runner — no Jest, no Mocha.
 *   - Uses `node:assert` for assertions.
 *   - Each fixture produces one subtest.
 *   - Put fixtures: feed data to Gun, read back graph state, compare.
 *   - Get fixtures: pre-seed Gun with data, perform get, compare.
 *   - Edge fixtures: verify Gun.js handles malformed input without crashing.
 *   - Divergences are reported as test failures with diagnostic output.
 */

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURES_DIR = join(__dirname, '..', 'wire', 'fixtures');

// ---------------------------------------------------------------------------
// Fixture loader (recursive, same as Rust harness)
// ---------------------------------------------------------------------------

function loadFixtures(dir, out = []) {
  for (const entry of readdirSync(dir)) {
    const path = join(dir, entry);
    const stat = statSync(path);
    if (stat.isDirectory()) {
      loadFixtures(path, out);
    } else if (entry.endsWith('.json')) {
      const content = readFileSync(path, 'utf-8');
      out.push(JSON.parse(content));
    }
  }
  return out;
}

// ---------------------------------------------------------------------------
// Gun.js harness
// ---------------------------------------------------------------------------

async function getGun() {
  const mod = await import('gun');
  return mod.default || mod;
}

/**
 * Put fixture: reconstruct the graph data from the fixture's expected values
 * and feed it to Gun.js.  Then read it back and compare.
 */
async function testPutFixture(t, fixture, Gun) {
  const expected = fixture.expected;
  if (!expected.parses) {
    t.skip('wire-parse-error fixture — not testable at Gun.js API level');
    return;
  }

  const gun = Gun({ localStorage: false, radisk: false });

  for (const soul of expected.souls || []) {
    const fields = expected.fields?.[soul] || [];
    const values = expected.values?.[soul] || {};

    // Build the data object for Gun.js
    const data = {};
    for (const key of fields) {
      const val = values[key];
      if (val !== null && typeof val === 'object' && val['#']) {
        // Relation link — Gun.js expects { '#': soul }
        data[key] = { '#': val['#'] };
      } else {
        data[key] = val;
      }
    }

    // If data is empty (e.g. missing_metadata fixture), skip — Gun.js needs
    // at least one field to create a node
    if (Object.keys(data).length === 0) {
      t.skip('no fields to put — Gun.js needs at least one field');
      return;
    }

    // Put the data into Gun
    gun.get(soul).put(data);

    // Read it back
    const result = await new Promise((resolve) => {
      gun.get(soul).once((node) => resolve(node));
    });

    assert.ok(result, `Gun.js returned null for soul ${soul}`);

    // Verify each field
    for (const key of fields) {
      const expectedVal = values[key];
      assert.ok(
        key in result,
        `field ${key} missing from Gun.js node ${soul}`
      );

      if (expectedVal !== null && typeof expectedVal === 'object' && expectedVal['#']) {
        // Relation — check the soul link
        assert.ok(
          result[key] && result[key]['#'] === expectedVal['#'],
          `relation mismatch for ${soul}.${key}: expected ${expectedVal['#']}, got ${result[key]?.['#']}`
        );
      } else {
        // Direct value — Gun.js stores numbers as-is, but may stringify
        // some types. Compare with type coercion for floats.
        const actual = result[key];
        if (typeof expectedVal === 'number') {
          assert.equal(actual, expectedVal, `value mismatch for ${soul}.${key}`);
        } else {
          assert.deepEqual(actual, expectedVal, `value mismatch for ${soul}.${key}`);
        }
      }
    }
  }
}

/**
 * Get fixture: pre-seed Gun with data and perform a get.
 */
async function testGetFixture(t, fixture, Gun) {
  const expected = fixture.expected;
  if (!expected.parses) {
    t.skip('wire-parse-error fixture — not testable at Gun.js API level');
    return;
  }

  const gun = Gun({ localStorage: false, radisk: false });

  const input = JSON.parse(fixture.input);
  const getSoul = input.get?.['#'];

  if (!getSoul) {
    t.skip('get fixture without soul — not testable at API level');
    return;
  }

  // Seed a test node and read it back
  gun.get(getSoul).put({ _test: 'mirror' });

  const result = await new Promise((resolve) => {
    gun.get(getSoul).once((node) => resolve(node));
  });

  assert.ok(result, `Gun.js get returned null for soul ${getSoul}`);
  assert.equal(result._test, 'mirror', `Gun.js get data mismatch`);
}

/**
 * Generic fixture (dam/handshake/batch): verify Gun.js works at API level.
 * These test BEAM's wire-format parser — Gun.js handles them internally.
 */
async function testGenericFixture(t, fixture, Gun) {
  const expected = fixture.expected;
  if (!expected.parses) {
    t.skip('wire-parse-error fixture — not testable at Gun.js API level');
    return;
  }

  // Verify Gun.js can be instantiated and basic operations work
  const gun = Gun({ localStorage: false, radisk: false });
  assert.ok(gun, 'Gun instance created');
}

// ---------------------------------------------------------------------------
// Main test runner
// ---------------------------------------------------------------------------

test('BEAM ↔ Gun.js wire mirror tests', async (t) => {
  const fixtures = loadFixtures(FIXTURES_DIR);

  assert.ok(fixtures.length > 0, 'no fixtures found');
  console.log(`Loaded ${fixtures.length} wire fixtures from ${FIXTURES_DIR}`);

  const Gun = await getGun();
  assert.ok(Gun, 'Gun.js loaded successfully');

  for (const fixture of fixtures) {
    await t.test(fixture.name, async (subt) => {
      const cat = fixture.category;
      if (cat === 'put') {
        await testPutFixture(subt, fixture, Gun);
      } else if (cat === 'get') {
        await testGetFixture(subt, fixture, Gun);
      } else {
        await testGenericFixture(subt, fixture, Gun);
      }
    });
  }

  console.log(`\nMirror tests complete: ${fixtures.length} fixtures checked`);
});