OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / tests / test-registry-graph.mjs

333 lines · javascript ref:
/**
 * test-registry-graph.mjs — Validates registry + graph routes
 *
 * Tests:
 *  1. registry.rooms returns array
 *  2. registry.add posts _hs:room-listing
 *  3. registry.list reads listings back
 *  4. registry.list filters by query
 *  5. registry.remove posts tombstone
 *  6. registry.list excludes tombstoned rooms
 *  7. graph.compute returns { clusters, edges, suggestions }
 *  8. graph.clusters returns array
 *  9. graph.edges returns array
 *
 * Requires: keet-daemon running on /tmp/keet-daemon.sock
 * Usage: bare tests/test-registry-graph.mjs
 */

import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import b4a from 'b4a'

const SOCKET = '/tmp/keet-daemon.sock'
let rpcId = 0
let passed = 0
let failed = 0

// --- RPC client ---

function connect () {
  const pipe = new Pipe(SOCKET)
  const framed = new FramedStream(pipe)
  const pending = new Map()

  framed.on('data', (buf) => {
    try {
      const msg = JSON.parse(b4a.toString(buf))
      const cb = pending.get(msg.id)
      if (cb) {
        pending.delete(msg.id)
        if (msg.ok !== undefined && msg.data !== undefined) {
          msg.result = msg.data
        }
        cb(msg)
      }
    } catch {}
  })

  function call (method, params = {}) {
    return new Promise((resolve, reject) => {
      const id = ++rpcId
      const timeout = setTimeout(() => {
        pending.delete(id)
        reject(new Error(`RPC timeout: ${method}`))
      }, 15000)
      pending.set(id, (msg) => {
        clearTimeout(timeout)
        if (msg.error) reject(new Error(msg.error.message || msg.error))
        else resolve(msg.result)
      })
      const payload = JSON.stringify({ jsonrpc: '2.0', id, method, params })
      framed.write(b4a.from(payload))
    })
  }

  function close () {
    framed.destroy()
    pipe.destroy()
  }

  return { call, close }
}

function assert (condition, label) {
  if (condition) {
    console.log(`  OK: ${label}`)
    passed++
  } else {
    console.log(`  FAIL: ${label}`)
    failed++
  }
}

function assertDeep (actual, expected, label) {
  const match = JSON.stringify(actual) === JSON.stringify(expected)
  if (match) {
    console.log(`  OK: ${label}`)
    passed++
  } else {
    console.log(`  FAIL: ${label}`)
    console.log(`    expected: ${JSON.stringify(expected)}`)
    console.log(`    actual:   ${JSON.stringify(actual)}`)
    failed++
  }
}

// --- Tests ---

async function run () {
  console.log('=== Registry + Graph Route Tests ===\n')

  const client = connect()

  // Get rooms to test with
  let rooms
  try {
    rooms = await client.call('rooms')
  } catch (err) {
    console.log('FAIL: Cannot list rooms:', err.message)
    client.close()
    Bare.exit(1)
  }

  if (!rooms || rooms.length === 0) {
    console.log('SKIP: No rooms available for testing')
    client.close()
    Bare.exit(0)
  }

  // Use dedicated test room (never pollute real rooms)
  let registryRoom = rooms.find(r => r.title === '_test-suite')
  if (!registryRoom) {
    await client.call('room.create', { title: '_test-suite' })
    await new Promise(r => setTimeout(r, 2000))
    const updated = await client.call('rooms')
    registryRoom = updated.find(r => r.title === '_test-suite')
  }
  if (!registryRoom) registryRoom = rooms[0]
  const registryRoomId = registryRoom.roomId || registryRoom.id
  console.log(`Registry room: "${registryRoom.title}" (${registryRoomId.slice(0, 12)}...)`)

  // Find a second room to use as "listed room"
  let listedRoom = rooms.find(r => (r.roomId || r.id) !== registryRoomId)
  if (!listedRoom) {
    console.log('SKIP: Need at least 2 rooms for registry tests')
    client.close()
    Bare.exit(0)
  }
  const listedRoomId = listedRoom.roomId || listedRoom.id
  console.log(`Listed room: "${listedRoom.title}" (${listedRoomId.slice(0, 12)}...)\n`)

  // === REGISTRY TESTS ===

  // T1: registry.rooms returns array
  console.log('--- T1: registry.rooms ---')
  try {
    const result = await client.call('registry.rooms')
    assert(Array.isArray(result), 'registry.rooms returns array')
    console.log(`  INFO: Found ${result.length} registry rooms`)
  } catch (err) {
    console.log(`  FAIL: registry.rooms threw: ${err.message}`)
    failed++
  }

  // T2: registry.add posts listing
  console.log('\n--- T2: registry.add ---')
  try {
    const result = await client.call('registry.add', {
      registryRoomId,
      roomId: listedRoomId,
      title: listedRoom.title || 'Test Room',
      description: 'Test listing for registry test',
      topics: ['testing', 'automated'],
      inviteToken: 'test-invite-token-123'
    })
    assert(result !== null && result !== undefined, 'registry.add returns result')
  } catch (err) {
    console.log(`  FAIL: registry.add threw: ${err.message}`)
    failed++
  }

  // T3: registry.list reads listings back
  console.log('\n--- T3: registry.list ---')
  try {
    const result = await client.call('registry.list', { registryRoomId })
    assert(Array.isArray(result), 'registry.list returns array')
    const found = result.find(r => r.roomId === listedRoomId)
    assert(found !== undefined, 'listed room found in results')
    if (found) {
      assert(found.title === (listedRoom.title || 'Test Room'), 'title matches')
      assertDeep(found.topics, ['testing', 'automated'], 'topics match')
      assert(found.inviteToken === 'test-invite-token-123', 'inviteToken matches')
      assert(found.listedAt !== null, 'listedAt timestamp set')
    }
  } catch (err) {
    console.log(`  FAIL: registry.list threw: ${err.message}`)
    failed++
  }

  // T4: registry.list filters by query
  console.log('\n--- T4: registry.list query filter ---')
  try {
    const result = await client.call('registry.list', {
      registryRoomId,
      query: 'automated'
    })
    assert(Array.isArray(result), 'filtered list returns array')
    const found = result.find(r => r.roomId === listedRoomId)
    assert(found !== undefined, 'room found by topic query')
  } catch (err) {
    console.log(`  FAIL: registry.list query threw: ${err.message}`)
    failed++
  }

  // T5: registry.list with non-matching query returns empty
  console.log('\n--- T5: registry.list non-matching query ---')
  try {
    const result = await client.call('registry.list', {
      registryRoomId,
      query: 'zzz_nonexistent_query_zzz'
    })
    assert(Array.isArray(result), 'filtered list returns array')
    assert(result.length === 0, 'no results for non-matching query')
  } catch (err) {
    console.log(`  FAIL: registry.list non-matching threw: ${err.message}`)
    failed++
  }

  // T6: registry.remove posts tombstone
  console.log('\n--- T6: registry.remove ---')
  try {
    const result = await client.call('registry.remove', {
      registryRoomId,
      roomId: listedRoomId
    })
    assert(result !== null, 'registry.remove returns result')
    assert(result.ok === true, 'registry.remove ok is true')
  } catch (err) {
    console.log(`  FAIL: registry.remove threw: ${err.message}`)
    failed++
  }

  // T7: registry.list excludes tombstoned rooms
  console.log('\n--- T7: registry.list after remove ---')
  try {
    const result = await client.call('registry.list', { registryRoomId })
    assert(Array.isArray(result), 'list returns array')
    const found = result.find(r => r.roomId === listedRoomId)
    assert(found === undefined, 'tombstoned room not in list')
  } catch (err) {
    console.log(`  FAIL: registry.list after remove threw: ${err.message}`)
    failed++
  }

  // T8: registry.add with missing params throws
  console.log('\n--- T8: registry.add missing params ---')
  try {
    await client.call('registry.add', { registryRoomId })
    console.log('  FAIL: should have thrown for missing roomId')
    failed++
  } catch (err) {
    assert(err.message.includes('roomId'), 'error mentions missing roomId')
  }

  // === GRAPH TESTS ===

  // T9: graph.compute returns structured result
  console.log('\n--- T9: graph.compute ---')
  try {
    const result = await client.call('graph.compute', { force: true })
    assert(result !== null && typeof result === 'object', 'graph.compute returns object')
    assert(Array.isArray(result.clusters), 'result has clusters array')
    assert(Array.isArray(result.edges), 'result has edges array')
    assert(Array.isArray(result.suggestions), 'result has suggestions array')
    console.log(`  INFO: ${result.clusters.length} clusters, ${result.edges.length} edges, ${result.suggestions.length} suggestions`)
  } catch (err) {
    console.log(`  FAIL: graph.compute threw: ${err.message}`)
    failed++
  }

  // T10: graph.clusters returns array
  console.log('\n--- T10: graph.clusters ---')
  try {
    const result = await client.call('graph.clusters')
    assert(Array.isArray(result), 'graph.clusters returns array')
    // Each cluster should have id, rooms, label, topics
    if (result.length > 0) {
      const c = result[0]
      assert(typeof c.id === 'number', 'cluster has numeric id')
      assert(Array.isArray(c.rooms), 'cluster has rooms array')
      assert(c.rooms.length >= 2, 'cluster has 2+ rooms (singletons excluded)')
    }
  } catch (err) {
    console.log(`  FAIL: graph.clusters threw: ${err.message}`)
    failed++
  }

  // T11: graph.edges returns array
  console.log('\n--- T11: graph.edges ---')
  try {
    const result = await client.call('graph.edges')
    assert(Array.isArray(result), 'graph.edges returns array')
    // Each edge should have from, to, mi, shared
    if (result.length > 0) {
      const e = result[0]
      assert(typeof e.from === 'string', 'edge has from string')
      assert(typeof e.to === 'string', 'edge has to string')
      assert(typeof e.mi === 'number', 'edge has numeric mi')
      assert(typeof e.shared === 'number', 'edge has numeric shared count')
      assert(e.mi > 0, 'mi is positive')
    }
  } catch (err) {
    console.log(`  FAIL: graph.edges threw: ${err.message}`)
    failed++
  }

  // T12: graph.compute without force uses cache (fast)
  console.log('\n--- T12: graph.compute cache ---')
  try {
    const start = Date.now()
    const result = await client.call('graph.compute', {})
    const elapsed = Date.now() - start
    assert(result !== null, 'cached compute returns result')
    assert(elapsed < 2000, `cached compute is fast (${elapsed}ms < 2000ms)`)
  } catch (err) {
    console.log(`  FAIL: graph.compute cache threw: ${err.message}`)
    failed++
  }

  // Summary
  console.log(`\n=== RESULT: ${passed} passed, ${failed} failed ===`)
  client.close()

  setTimeout(() => {
    Bare.exit(failed > 0 ? 1 : 0)
  }, 500)
}

run().catch(err => {
  console.log('FATAL:', err.message)
  Bare.exit(1)
})