/**
* metadata-base.test.mjs — Tests for the MetadataBase Autobase smart contract
*
* Run: bare test/metadata-base.test.mjs
*
* Tests the core Autobase module in isolation (no keet-core dependency).
* Uses tmp directories for storage, cleaned up on completion.
*/
import { MetadataBase, mergeFields } from '../lib/metadata-base.js'
import { RoomMeta, parseHsMessage, createHsMessage } from '../lib/room-meta.js'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
const tmpBase = path.join(os.tmpdir(), 'metadata-test-' + Date.now())
let testCount = 0
let passCount = 0
let failCount = 0
function assert (condition, msg) {
testCount++
if (condition) {
passCount++
console.log(` PASS: ${msg}`)
} else {
failCount++
console.error(` FAIL: ${msg}`)
}
}
function assertEqual (actual, expected, msg) {
testCount++
const pass = JSON.stringify(actual) === JSON.stringify(expected)
if (pass) {
passCount++
console.log(` PASS: ${msg}`)
} else {
failCount++
console.error(` FAIL: ${msg}`)
console.error(` expected: ${JSON.stringify(expected)}`)
console.error(` actual: ${JSON.stringify(actual)}`)
}
}
function tmpDir (name) {
const dir = path.join(tmpBase, name)
fs.mkdirSync(dir, { recursive: true })
return dir
}
function rmDir (dir) {
try {
fs.rmSync(dir, { recursive: true, force: true })
} catch {
// Best effort cleanup
}
}
// ── Test: mergeFields helper ─────────────────────────────────
console.log('\n--- mergeFields ---')
{
const result = mergeFields(null, { topics: ['rust'], discoverable: true }, 100)
assertEqual(result.topics, ['rust'], 'merge into null: topics set')
assertEqual(result.discoverable, true, 'merge into null: discoverable set')
assertEqual(result._fieldTs.topics, 100, 'merge into null: field timestamp recorded')
}
{
const existing = {
topics: ['rust'],
discoverable: true,
_fieldTs: { topics: 100, discoverable: 100 }
}
const result = mergeFields(existing, { topics: ['go', 'p2p'] }, 200)
assertEqual(result.topics, ['go', 'p2p'], 'newer timestamp wins: topics updated')
assertEqual(result.discoverable, true, 'unchanged field preserved')
assertEqual(result._fieldTs.topics, 200, 'field timestamp updated')
}
{
const existing = {
topics: ['rust'],
discoverable: true,
_fieldTs: { topics: 200, discoverable: 100 }
}
const result = mergeFields(existing, { topics: ['old'] }, 150)
assertEqual(result.topics, ['rust'], 'older timestamp loses: topics unchanged')
assertEqual(result._fieldTs.topics, 200, 'field timestamp unchanged')
}
{
const existing = {
topics: ['rust'],
type: 'chat',
_fieldTs: { topics: 100, type: 100 }
}
const result = mergeFields(existing, { type: null }, 200)
assertEqual(result.type, undefined, 'null value deletes field')
assertEqual(result.topics, ['rust'], 'other fields preserved')
}
// ── Test: MetadataBase lifecycle ─────────────────────────────
console.log('\n--- MetadataBase lifecycle ---')
{
const dir = tmpDir('lifecycle')
const mb = new MetadataBase(dir, { network: false })
assert(!mb._ready, 'not ready before ready()')
await mb.ready()
assert(mb._ready, 'ready after ready()')
assert(mb.key !== null, 'has bootstrap key')
assert(mb.localKey !== null, 'has local writer key')
assert(mb.writable, 'is writable (creator is always writer)')
// Key persisted to disk
const savedKey = fs.readFileSync(path.join(dir, 'metadata-key'), 'utf8').trim()
assertEqual(savedKey, mb.key.toString('hex'), 'key persisted to disk')
await mb.close()
assert(!mb._ready, 'not ready after close()')
rmDir(dir)
}
// ── Test: Key persistence across restarts ────────────────────
console.log('\n--- Key persistence ---')
{
const dir = tmpDir('persistence')
// First run: create new
const mb1 = new MetadataBase(dir, { network: false })
await mb1.ready()
const key1 = mb1.key.toString('hex')
await mb1.close()
// Second run: should load same key
const mb2 = new MetadataBase(dir, { network: false })
await mb2.ready()
const key2 = mb2.key.toString('hex')
assertEqual(key2, key1, 'same key after restart')
await mb2.close()
rmDir(dir)
}
// ── Test: Basic get/set ──────────────────────────────────────
console.log('\n--- Basic get/set ---')
{
const dir = tmpDir('basic')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
// Set room metadata
await mb.set('room', 'abc123', { topics: ['rust', 'p2p'], discoverable: true })
// Get it back
const result = await mb.get('room', 'abc123')
assertEqual(result.topics, ['rust', 'p2p'], 'get returns topics')
assertEqual(result.discoverable, true, 'get returns discoverable')
assert(result._ts > 0, 'has timestamp')
assert(result._by !== undefined, 'has writer attribution')
// Get nonexistent
const missing = await mb.get('room', 'nonexistent')
assertEqual(missing, null, 'nonexistent returns null')
await mb.close()
rmDir(dir)
}
// ── Test: Field-level merge ──────────────────────────────────
console.log('\n--- Field-level merge ---')
{
const dir = tmpDir('merge')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
// Set initial metadata
await mb.set('room', 'abc', { topics: ['rust'], type: 'chat' })
// Update only discoverable (topics should be preserved)
await mb.set('room', 'abc', { discoverable: true })
const result = await mb.get('room', 'abc')
assertEqual(result.topics, ['rust'], 'original field preserved')
assertEqual(result.type, 'chat', 'original field preserved')
assertEqual(result.discoverable, true, 'new field added')
// Delete a field with null
await mb.set('room', 'abc', { type: null })
const result2 = await mb.get('room', 'abc')
assertEqual(result2.type, undefined, 'null deletes field')
assertEqual(result2.topics, ['rust'], 'other fields preserved')
await mb.close()
rmDir(dir)
}
// ── Test: Namespace extensibility ────────────────────────────
console.log('\n--- Namespace extensibility ---')
{
const dir = tmpDir('namespaces')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
// Room metadata (known namespace)
await mb.set('room', 'room1', { topics: ['gaming'] })
// Bridge config (known namespace)
await mb.append({
ns: 'bridge',
roomId: 'room1',
platform: 'irc',
config: { network: 'libera', channel: '#gaming' }
})
// Governance (known namespace)
await mb.append({
ns: 'gov',
roomId: 'room1',
rule: 'voting',
params: { threshold: 0.66, quorum: 3 }
})
// Unknown namespace (forward compatibility — stored in default case)
await mb.set('reputation', 'user1', { score: 4.2, reviews: 10 })
// All are queryable
const room = await mb.get('room', 'room1')
assertEqual(room.topics, ['gaming'], 'room namespace works')
const bridge = await mb.get('bridge', 'room1:irc')
assertEqual(bridge.network, 'libera', 'bridge namespace works')
const gov = await mb.get('gov', 'room1:voting')
assertEqual(gov.threshold, 0.66, 'gov namespace works')
const rep = await mb.get('reputation', 'user1')
assertEqual(rep.score, 4.2, 'unknown namespace stored via default case')
assertEqual(rep._ns, 'reputation', 'unknown namespace marked with _ns')
await mb.close()
rmDir(dir)
}
// ── Test: getAll ─────────────────────────────────────────────
console.log('\n--- getAll ---')
{
const dir = tmpDir('getall')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
await mb.set('room', 'r1', { topics: ['a'] })
await mb.set('room', 'r2', { topics: ['b'] })
await mb.set('room', 'r3', { topics: ['c'] })
const all = await mb.getAll('room')
assert(Object.keys(all).length === 3, 'getAll returns 3 entries')
assertEqual(all.r1.topics, ['a'], 'r1 correct')
assertEqual(all.r2.topics, ['b'], 'r2 correct')
assertEqual(all.r3.topics, ['c'], 'r3 correct')
// Different namespace not included
await mb.set('bridge', 'b1', { platform: 'irc' })
const rooms = await mb.getAll('room')
assert(Object.keys(rooms).length === 3, 'getAll scoped to namespace')
await mb.close()
rmDir(dir)
}
// ── Test: Topic search (secondary index) ─────────────────────
console.log('\n--- Topic search ---')
{
const dir = tmpDir('search')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
await mb.set('room', 'r1', { topics: ['rust', 'p2p'] })
await mb.set('room', 'r2', { topics: ['rust', 'gaming'] })
await mb.set('room', 'r3', { topics: ['python', 'ml'] })
const rustRooms = await mb.searchByTopic('rust')
assertEqual(rustRooms.length, 2, 'found 2 rust rooms')
const rustIds = rustRooms.map(r => r.roomId).sort()
assertEqual(rustIds, ['r1', 'r2'], 'correct rooms for rust topic')
const pythonRooms = await mb.searchByTopic('python')
assertEqual(pythonRooms.length, 1, 'found 1 python room')
assertEqual(pythonRooms[0].roomId, 'r3', 'correct room for python')
const noRooms = await mb.searchByTopic('java')
assertEqual(noRooms.length, 0, 'no rooms for unknown topic')
// Update topics — old index entries should be cleaned
await mb.set('room', 'r1', { topics: ['go'] })
const rustAfter = await mb.searchByTopic('rust')
assertEqual(rustAfter.length, 1, 'index updated: only 1 rust room after change')
assertEqual(rustAfter[0].roomId, 'r2', 'correct remaining rust room')
const goRooms = await mb.searchByTopic('go')
assertEqual(goRooms.length, 1, 'new topic indexed')
assertEqual(goRooms[0].roomId, 'r1', 'correct room for new topic')
await mb.close()
rmDir(dir)
}
// ── Test: Delete operation ───────────────────────────────────
console.log('\n--- Delete ---')
{
const dir = tmpDir('delete')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
await mb.set('room', 'r1', { topics: ['rust'] })
const before = await mb.get('room', 'r1')
assert(before !== null, 'entry exists before delete')
await mb.del('room:r1')
const after = await mb.get('room', 'r1')
assertEqual(after, null, 'entry gone after delete')
await mb.close()
rmDir(dir)
}
// ── Test: List prefix query ──────────────────────────────────
console.log('\n--- List prefix ---')
{
const dir = tmpDir('list')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
await mb.append({ ns: 'bridge', roomId: 'r1', platform: 'irc', config: { ch: '#a' } })
await mb.append({ ns: 'bridge', roomId: 'r1', platform: 'discord', config: { ch: 'b' } })
await mb.append({ ns: 'bridge', roomId: 'r2', platform: 'irc', config: { ch: '#c' } })
const r1Bridges = await mb.list('bridge:r1:')
assertEqual(r1Bridges.length, 2, 'list returns 2 bridges for r1')
const allBridges = await mb.list('bridge:')
assertEqual(allBridges.length, 3, 'list returns all 3 bridges')
await mb.close()
rmDir(dir)
}
// ── Test: Data persists across restart ───────────────────────
console.log('\n--- Data persistence ---')
{
const dir = tmpDir('persist-data')
// Write data
const mb1 = new MetadataBase(dir, { network: false })
await mb1.ready()
await mb1.set('room', 'persist-test', { topics: ['durable'], discoverable: true })
await mb1.close()
// Read after restart
const mb2 = new MetadataBase(dir, { network: false })
await mb2.ready()
const result = await mb2.get('room', 'persist-test')
assertEqual(result.topics, ['durable'], 'data survives restart')
assertEqual(result.discoverable, true, 'all fields survive restart')
await mb2.close()
rmDir(dir)
}
// ── Test: RoomMeta wrapper ───────────────────────────────────
console.log('\n--- RoomMeta wrapper ---')
{
const dir = tmpDir('roommeta')
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
const rm = new RoomMeta(mb)
// Set via RoomMeta
await rm.set('abc', { topics: ['rust'], discoverable: true })
// Get via RoomMeta (should strip internal fields)
const result = await rm.get('abc')
assertEqual(result.topics, ['rust'], 'RoomMeta.get returns topics')
assertEqual(result.discoverable, true, 'RoomMeta.get returns discoverable')
assertEqual(result._by, undefined, 'internal _by stripped')
assertEqual(result._ts, undefined, 'internal _ts stripped')
assertEqual(result._fieldTs, undefined, 'internal _fieldTs stripped')
// GetAll via RoomMeta
await rm.set('def', { topics: ['go'], type: 'registry' })
const all = await rm.getAll()
assert(Object.keys(all).length === 2, 'getAll returns 2 rooms')
assertEqual(all.abc.topics, ['rust'], 'getAll abc correct')
assertEqual(all.def.type, 'registry', 'getAll def correct')
assertEqual(all.def._by, undefined, 'getAll strips internals')
// Search via RoomMeta
const rustRooms = await rm.searchByTopic('rust')
assertEqual(rustRooms.length, 1, 'searchByTopic works through RoomMeta')
// Get nonexistent
const empty = await rm.get('nonexistent')
assertEqual(JSON.stringify(empty), '{}', 'nonexistent returns {}')
await mb.close()
rmDir(dir)
}
// ── Test: parseHsMessage / createHsMessage helpers ───────────
console.log('\n--- _hs message helpers ---')
{
const msg = createHsMessage('room-meta', { roomId: 'abc', topics: ['rust'] })
assert(msg.startsWith('{"_hs":'), 'createHsMessage starts with _hs prefix')
const parsed = parseHsMessage(msg)
assertEqual(parsed._hs, 'room-meta', 'parseHsMessage extracts type')
assertEqual(parsed.roomId, 'abc', 'parseHsMessage extracts roomId')
assertEqual(parsed.topics, ['rust'], 'parseHsMessage extracts topics')
// Invalid inputs
assertEqual(parseHsMessage(null), null, 'null returns null')
assertEqual(parseHsMessage('hello world'), null, 'non-hs returns null')
assertEqual(parseHsMessage('{"_hs":broken'), null, 'invalid JSON returns null')
}
// ── Test: Legacy migration ───────────────────────────────────
console.log('\n--- Legacy migration ---')
{
const dir = tmpDir('migration')
const home = tmpDir('migration-home')
// Create legacy room-meta.json
const configDir = path.join(home, '.config', 'keet-cli')
fs.mkdirSync(configDir, { recursive: true })
const legacyData = {
room1: { topics: ['rust'], discoverable: true, _syncTs: 123 },
room2: { topics: ['go'], type: 'registry' }
}
fs.writeFileSync(path.join(configDir, 'room-meta.json'), JSON.stringify(legacyData))
// Create MetadataBase + RoomMeta with legacy path
const mb = new MetadataBase(dir, { network: false })
await mb.ready()
const rm = new RoomMeta(mb, { storagePath: home })
// Run migration
const { migrated } = await rm.migrateLegacy(null)
assertEqual(migrated, 2, 'migrated 2 entries')
// Verify data is in Autobase
const r1 = await rm.get('room1')
assertEqual(r1.topics, ['rust'], 'migrated room1 topics')
assertEqual(r1.discoverable, true, 'migrated room1 discoverable')
assertEqual(r1._syncTs, undefined, 'legacy _syncTs stripped')
const r2 = await rm.get('room2')
assertEqual(r2.type, 'registry', 'migrated room2 type')
// Legacy file should be renamed
const backupExists = fs.existsSync(path.join(configDir, 'room-meta.json.migrated'))
assert(backupExists, 'legacy file renamed to .migrated')
// Second migration is a no-op
const { migrated: m2 } = await rm.migrateLegacy(null)
assertEqual(m2, 0, 'second migration is no-op')
await mb.close()
rmDir(dir)
rmDir(home)
}
// ── Test: Independent identity (no mnemonic derivation) ──────
console.log('\n--- Independent identity ---')
{
const dir1 = tmpDir('indep-1')
const dir2 = tmpDir('indep-2')
// Two MetadataBases with different storage paths get different keys
const mb1 = new MetadataBase(dir1, { network: false })
await mb1.ready()
const mb2 = new MetadataBase(dir2, { network: false })
await mb2.ready()
assert(mb1.key.toString('hex') !== mb2.key.toString('hex'), 'different storage → different Autobase key')
assert(mb1.localKey.toString('hex') !== mb2.localKey.toString('hex'), 'local writer keys are unique per instance')
// Both are writable (creators are always writers)
assert(mb1.writable, 'instance 1 is writable')
assert(mb2.writable, 'instance 2 is writable')
await mb1.close()
await mb2.close()
rmDir(dir1)
rmDir(dir2)
}
// ── Test: Key recovery from Corestore ─────────────────────────
console.log('\n--- Key recovery from Corestore ---')
{
const dir = tmpDir('recovery')
// Create MetadataBase, write data, close
const mb1 = new MetadataBase(dir, { network: false })
await mb1.ready()
const originalKey = mb1.key.toString('hex')
await mb1.set('room', 'recovery-test', { topics: ['test'] })
await mb1.close()
// Delete the metadata-key file (simulating file loss)
const keyFile = path.join(dir, 'metadata-key')
assert(fs.existsSync(keyFile), 'metadata-key file exists before delete')
fs.unlinkSync(keyFile)
assert(!fs.existsSync(keyFile), 'metadata-key file deleted')
// Re-open — Autobase key should be re-derived from Corestore
// (null bootstrapKey + no key file → Autobase creates from existing store)
const mb2 = new MetadataBase(dir, { network: false })
await mb2.ready()
const recoveredKey = mb2.key.toString('hex')
// Key file should be re-created
assert(fs.existsSync(keyFile), 'metadata-key file re-created after recovery')
// Note: without the key file, Autobase may create a NEW key from the store.
// The important thing is that it starts up successfully and the key is persisted.
assert(typeof recoveredKey === 'string' && recoveredKey.length === 64, 'recovered key is valid 64-char hex')
assert(mb2.writable, 'recovered instance is writable')
await mb2.close()
rmDir(dir)
}
// ── Test: joinKey (device pairing) ────────────────────────────
console.log('\n--- joinKey (device pairing) ---')
{
const dirA = tmpDir('pair-a')
const dirB = tmpDir('pair-b')
// Device A creates its MetadataBase and writes data
const mbA = new MetadataBase(dirA, { network: false })
await mbA.ready()
const keyA = mbA.key.toString('hex')
await mbA.set('room', 'shared-room', { topics: ['synced'], discoverable: true })
// Device B creates its own (different key)
const mbB = new MetadataBase(dirB, { network: false })
await mbB.ready()
const keyB = mbB.key.toString('hex')
assert(keyA !== keyB, 'devices start with different keys')
// Device B joins Device A's MetadataBase
await mbB.joinKey(Buffer.from(keyA, 'hex'))
assertEqual(mbB.key.toString('hex'), keyA, 'after joinKey, B has A\'s key')
// Note: B is NOT writable without networking — Protomux writer exchange
// (which adds B as an indexer) requires a live Hyperswarm connection.
// In production with network: true, both peers become writable automatically.
assert(!mbB.writable, 'B is not writable without network (expected — needs Protomux)')
// Key file updated on disk
const savedKey = fs.readFileSync(path.join(dirB, 'metadata-key'), 'utf8').trim()
assertEqual(savedKey, keyA, 'metadata-key file updated to A\'s key')
// joinKey with same key is a no-op (doesn't throw)
await mbB.joinKey(Buffer.from(keyA, 'hex'))
assertEqual(mbB.key.toString('hex'), keyA, 'joinKey same key is no-op')
// joinKey rejects invalid keys
let threw = false
try { await mbB.joinKey(Buffer.alloc(16)) } catch { threw = true }
assert(threw, 'joinKey rejects non-32-byte key')
await mbA.close()
await mbB.close()
rmDir(dirA)
rmDir(dirB)
}
// ── Test: joinKey persists across restart ──────────────────────
console.log('\n--- joinKey persists across restart ---')
{
const dirA = tmpDir('persist-a')
const dirB = tmpDir('persist-b')
// Device A
const mbA = new MetadataBase(dirA, { network: false })
await mbA.ready()
const keyA = mbA.key.toString('hex')
// Device B joins A, then closes
const mbB1 = new MetadataBase(dirB, { network: false })
await mbB1.ready()
await mbB1.joinKey(Buffer.from(keyA, 'hex'))
await mbB1.close()
// Device B restarts — should still use A's key
const mbB2 = new MetadataBase(dirB, { network: false })
await mbB2.ready()
assertEqual(mbB2.key.toString('hex'), keyA, 'joined key persists across restart')
await mbA.close()
await mbB2.close()
rmDir(dirA)
rmDir(dirB)
}
// ── Summary ──────────────────────────────────────────────────
console.log(`\n${'='.repeat(50)}`)
console.log(`Results: ${passCount}/${testCount} passed, ${failCount} failed`)
console.log('='.repeat(50))
// Cleanup tmp base
rmDir(tmpBase)
if (failCount > 0) {
console.error('\nSOME TESTS FAILED')
Bare.exit(1)
} else {
console.log('\nALL TESTS PASSED')
}