OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / tests / test-phase-b-meta.mjs

261 lines · javascript ref:
/**
 * test-phase-b-meta.mjs — Validates Phase B: room metadata round-trip
 *
 * Tests:
 * 1. meta.set writes topics + discoverable to a room
 * 2. meta.get reads them back correctly
 * 3. meta.set merges (doesn't overwrite unrelated keys)
 * 4. topic.announce works for discoverable rooms
 * 5. topic.search finds announced rooms
 *
 * Requires: keet-daemon running on /tmp/keet-daemon.sock
 * Usage: bare tests/test-phase-b-meta.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)
        // Normalize: daemon uses {ok, data} format, not {result}
        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('=== Phase B: Room Metadata Tests ===\n')

  const client = connect()

  // Get a room 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 testRoom = rooms.find(r => r.title === '_test-suite')
  if (!testRoom) {
    await client.call('room.create', { title: '_test-suite' })
    await new Promise(r => setTimeout(r, 2000))
    const updated = await client.call('rooms')
    testRoom = updated.find(r => r.title === '_test-suite')
  }
  if (!testRoom) testRoom = rooms[0]
  const roomId = testRoom.roomId || testRoom.id
  console.log(`Using room: "${testRoom.title}" (${roomId.slice(0, 12)}...)\n`)

  // T1: meta.set with topics + discoverable
  console.log('--- T1: meta.set (topics + discoverable) ---')
  try {
    const result = await client.call('meta.set', {
      roomId,
      metadata: { topics: ['test', 'p2p', 'gaming'], discoverable: true }
    })
    assert(result !== null && result !== undefined, 'meta.set returns result')
    assert(Array.isArray(result.topics), 'result has topics array')
    assertDeep(result.topics, ['test', 'p2p', 'gaming'], 'topics match input')
    assert(result.discoverable === true, 'discoverable is true')
  } catch (err) {
    console.log(`  FAIL: meta.set threw: ${err.message}`)
    failed++
  }

  // T2: meta.get reads back
  console.log('\n--- T2: meta.get (read back) ---')
  try {
    const meta = await client.call('meta.get', { roomId })
    assert(meta !== null && typeof meta === 'object', 'meta.get returns object')
    assertDeep(meta.topics, ['test', 'p2p', 'gaming'], 'topics persisted correctly')
    assert(meta.discoverable === true, 'discoverable persisted correctly')
  } catch (err) {
    console.log(`  FAIL: meta.get threw: ${err.message}`)
    failed++
  }

  // T3: meta.set merges (add new key, keep existing)
  console.log('\n--- T3: meta.set merge behavior ---')
  try {
    const result = await client.call('meta.set', {
      roomId,
      metadata: { customField: 'hello' }
    })
    assert(result.customField === 'hello', 'new field added')
    assertDeep(result.topics, ['test', 'p2p', 'gaming'], 'existing topics preserved')
    assert(result.discoverable === true, 'existing discoverable preserved')
  } catch (err) {
    console.log(`  FAIL: meta.set merge threw: ${err.message}`)
    failed++
  }

  // T4: meta.set can update topics
  console.log('\n--- T4: meta.set update topics ---')
  try {
    const result = await client.call('meta.set', {
      roomId,
      metadata: { topics: ['rust', 'hypercore'] }
    })
    assertDeep(result.topics, ['rust', 'hypercore'], 'topics updated')
    assert(result.discoverable === true, 'discoverable unchanged')
    assert(result.customField === 'hello', 'customField unchanged')
  } catch (err) {
    console.log(`  FAIL: meta.set update threw: ${err.message}`)
    failed++
  }

  // T5: meta.get after update
  console.log('\n--- T5: meta.get after update ---')
  try {
    const meta = await client.call('meta.get', { roomId })
    assertDeep(meta.topics, ['rust', 'hypercore'], 'updated topics read back')
    assert(meta.discoverable === true, 'discoverable still true')
  } catch (err) {
    console.log(`  FAIL: meta.get after update threw: ${err.message}`)
    failed++
  }

  // T6: meta.set null removes field
  console.log('\n--- T6: meta.set null removes field ---')
  try {
    const result = await client.call('meta.set', {
      roomId,
      metadata: { customField: null }
    })
    assert(result.customField === undefined, 'null field removed')
    assertDeep(result.topics, ['rust', 'hypercore'], 'topics still present')
  } catch (err) {
    console.log(`  FAIL: meta.set null threw: ${err.message}`)
    failed++
  }

  // T7: topic.announce for discoverable room
  console.log('\n--- T7: topic.announce ---')
  try {
    const result = await client.call('topic.announce', { roomId })
    assert(result !== null, 'topic.announce returns result')
    assert(Array.isArray(result.topics), 'result has topics array')
  } catch (err) {
    console.log(`  FAIL: topic.announce threw: ${err.message}`)
    failed++
  }

  // T8: topic.search finds announced room
  console.log('\n--- T8: topic.search ---')
  try {
    const result = await client.call('topic.search', { query: 'rust' })
    assert(Array.isArray(result), 'topic.search returns array')
    // May or may not find results immediately (DHT propagation delay)
    console.log(`  INFO: Found ${result.length} results for "rust"`)
  } catch (err) {
    console.log(`  FAIL: topic.search threw: ${err.message}`)
    failed++
  }

  // Cleanup: restore empty topics
  console.log('\n--- Cleanup ---')
  try {
    await client.call('meta.set', {
      roomId,
      metadata: { topics: [], discoverable: false, customField: null }
    })
    console.log('  OK: Cleaned up test metadata')
  } catch (err) {
    console.log(`  WARN: Cleanup failed: ${err.message}`)
  }

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

  // Give framed-stream time to flush
  setTimeout(() => {
    Bare.exit(failed > 0 ? 1 : 0)
  }, 500)
}

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