/**
* test-admin-channel.mjs — MetadataBase Autobase sync tests
*
* Tests:
* 1. meta.key returns independent identity key info
* 2. meta.get reads from Autobase after set
* 3. room.create with roomType creates correct type
* 4. room.create without roomType creates Default room
* 5. meta.set with null field removes it
* 6. rapid meta.set calls don't corrupt (last-write-wins)
* 7. meta.set for multiple rooms stores separately
* 8. meta.get for unknown roomId returns empty object
* 9. meta.get is consistent after multiple operations
* 10. meta.invite returns 64-char hex pairing key
* 11. meta.join rejects invalid key, self-join is no-op
*
* Requires: keet-daemon running on /tmp/keet-daemon.sock
* Usage: bare tests/test-admin-channel.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
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 = {}, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const id = ++rpcId
const timeout = setTimeout(() => {
pending.delete(id)
reject(new Error(`RPC timeout: ${method}`))
}, timeoutMs)
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++ }
}
async function run () {
console.log('=== MetadataBase Autobase Sync Tests ===\n')
const client = connect()
// Find or create dedicated test room (never pollute real rooms)
const rooms = await client.call('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) {
console.log('FAIL: Could not find or create _test-suite room')
client.close()
Bare.exit(1)
}
const roomId = testRoom.roomId || testRoom.id
// Second test room: pick any other room
const chatRooms = rooms.filter(r => {
const id = r.roomId || r.id
return id !== roomId
})
const secondRoom = chatRooms.length > 0 ? chatRooms[0] : null
console.log(`Test room: "${testRoom.title}" (${roomId.slice(0, 12)}...)\n`)
const marker = 'regression-' + Date.now()
// === T1: meta.key returns independent identity key ===
console.log('--- T1: meta.key returns independent identity key ---')
try {
const keyResult = await client.call('meta.key')
assert(keyResult.key !== null, 'meta.key returns bootstrap key')
assert(keyResult.writable === true, 'local peer is writable')
assert(keyResult.localKey !== null, 'has local writer key')
assert(typeof keyResult.key === 'string' && keyResult.key.length === 64, 'key is 64-char hex')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T2: meta.get reads Autobase after set ===
console.log('\n--- T2: meta.get reads Autobase after set ---')
try {
await client.call('meta.set', {
roomId,
metadata: { topics: [marker], discoverable: true, category: 'test' }
})
const meta = await client.call('meta.get', { roomId })
assert(meta.topics?.includes(marker), 'Autobase has marker topic')
assert(meta.discoverable === true, 'discoverable preserved')
assert(meta.category === 'test', 'category preserved')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T3: room.create with roomType ===
console.log('\n--- T3: room.create with roomType=1 (Broadcast) ---')
let broadcastId = null
try {
const result = await client.call('room.create', { title: '_test-broadcast-' + Date.now(), roomType: '1' })
broadcastId = typeof result === 'string' ? result : (result?.roomId || result?.id || result)
assert(broadcastId !== null, 'room.create returned roomId')
// keet-core internal: digest.settings.roomType 2 = Broadcast
const info = await client.call('room', { roomId: broadcastId })
const rt = info.digest?.settings?.roomType
assert(rt === 2, `roomType is Broadcast (digest=${rt})`)
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T4: room.create without roomType ===
console.log('\n--- T4: room.create without roomType (Default) ---')
let defaultId = null
try {
const result = await client.call('room.create', { title: '_test-default-' + Date.now() })
defaultId = typeof result === 'string' ? result : (result?.roomId || result?.id || result)
assert(defaultId !== null, 'room.create returned roomId')
// keet-core internal: digest.settings.roomType 1 = Default
const info = await client.call('room', { roomId: defaultId })
const rt = info.digest?.settings?.roomType
assert(rt === 1, `roomType is Default (digest=${rt})`)
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T5: meta.set null removes field ===
console.log('\n--- T5: meta.set null removes field ---')
try {
await client.call('meta.set', { roomId, metadata: { category: null } })
const meta = await client.call('meta.get', { roomId })
assert(meta.category === undefined, 'null field removed')
assert(meta.topics?.includes(marker), 'other fields preserved')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T6: rapid meta.set (last-write-wins) ===
console.log('\n--- T6: rapid meta.set last-write-wins ---')
try {
// Fire 3 rapid sets
await client.call('meta.set', { roomId, metadata: { topics: ['first'] } })
await client.call('meta.set', { roomId, metadata: { topics: ['second'] } })
await client.call('meta.set', { roomId, metadata: { topics: ['third'] } })
const meta = await client.call('meta.get', { roomId })
assert(meta.topics?.includes('third'), 'last write wins')
assert(!meta.topics?.includes('first'), 'earlier writes overwritten')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T7: meta.set for multiple rooms ===
console.log('\n--- T7: multiple rooms store separately ---')
if (secondRoom) {
const secondId = secondRoom.roomId || secondRoom.id
try {
const marker2 = 'multi-' + Date.now()
await client.call('meta.set', { roomId, metadata: { topics: [marker2] } })
await client.call('meta.set', { roomId: secondId, metadata: { topics: [marker2 + '-b'] } })
const meta1 = await client.call('meta.get', { roomId })
const meta2 = await client.call('meta.get', { roomId: secondId })
assert(meta1.topics?.includes(marker2), 'room 1 has its marker')
assert(meta2.topics?.includes(marker2 + '-b'), 'room 2 has its marker')
assert(!meta1.topics?.includes(marker2 + '-b'), 'room 1 does not have room 2 marker')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
} else {
console.log(' SKIP: only 1 chat room available')
}
// === T8: meta.get for unknown roomId ===
console.log('\n--- T8: meta.get unknown roomId returns {} ---')
try {
const meta = await client.call('meta.get', { roomId: 'nonexistent-room-id-12345' })
assert(typeof meta === 'object', 'returns object')
assert(Object.keys(meta).length === 0, 'empty object for unknown room')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T9: meta.get consistency ===
console.log('\n--- T9: meta.get consistent after operations ---')
try {
const meta = await client.call('meta.get', { roomId })
assert(meta.topics !== undefined, 'has topics after all operations')
assert(typeof meta === 'object', 'returns object not array')
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T10: meta.invite returns 64-char hex key ===
console.log('\n--- T10: meta.invite returns pairing key ---')
let inviteKey = null
try {
const result = await client.call('meta.invite')
assert(typeof result === 'object', 'meta.invite returns object')
assert(typeof result.key === 'string', 'has key field')
assert(result.key.length === 64, 'key is 64-char hex')
inviteKey = result.key
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === T11: meta.join validation and self-join ===
console.log('\n--- T11: meta.join validation + self-join ---')
try {
// Invalid key should fail
let joinFailed = false
try {
await client.call('meta.join', { key: 'too-short' })
} catch {
joinFailed = true
}
assert(joinFailed, 'meta.join rejects invalid key')
// Join own key should be a no-op (already a writer)
if (inviteKey) {
const result = await client.call('meta.join', { key: inviteKey })
assert(typeof result === 'object', 'self-join returns object')
assert(result.key === inviteKey || result.ok !== undefined, 'self-join does not error')
}
} catch (err) {
console.log(` FAIL: ${err.message}`)
failed++
}
// === Cleanup ===
console.log('\n--- Cleanup ---')
try {
await client.call('meta.set', { roomId, metadata: { topics: [], discoverable: false, category: null } })
if (secondRoom) {
const secondId = secondRoom.roomId || secondRoom.id
await client.call('meta.set', { roomId: secondId, metadata: { topics: [], discoverable: false } })
}
// Leave test rooms (broadcast + default)
if (broadcastId) {
try { await client.call('room.leave', { roomId: broadcastId }) } catch {}
}
if (defaultId) {
try { await client.call('room.leave', { roomId: defaultId }) } catch {}
}
console.log(' OK: Cleaned up test metadata + rooms')
} catch (err) {
console.log(` WARN: Cleanup failed: ${err.message}`)
}
// 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)
})