// identity-bootstrap-full.test.js — end-to-end test of the post-autobase-fix
// identity bootstrap flow PLUS displayName + username steps. Drives the same
// RPCs that hyper-tps's `identity create --fresh --skip-registry` uses,
// against fresh tmp storage, and verifies on-disk + in-memory end state.
//
// Why --skip-registry equivalent? Keet's user registry is a single global
// autobase — every successful identity.register is a PERMANENT claim. Tests
// must never call identity.register against the production registry. This
// test exercises identity.checkName (read-only) but skips register.
//
// Steps:
// 1. startDaemon on a tmp dir (no real ~/.config/keet-cli touched)
// 2. identity-manager.list/create/switch to a new identity
// 3. vault.setup → mnemonic.enc written, entropy wiped, identity confirmed
// 4. identity.setName <random> → displayName persists in profile
// 5. identity.checkName <random gibberish> → returns boolean (read-only,
// does NOT claim the name)
// 6. identity → final shape has publicKey + displayName populated
//
// Run: cd hyper-fleeky/packages/keet-daemon && bare test/identity-bootstrap-full.test.js
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import { startDaemon } from '../index.js'
let exitCode = 0
const failures = []
function ok (cond, msg) {
if (cond) console.log(' ok ' + msg)
else { console.log(' FAIL ' + msg); exitCode = 1; failures.push(msg) }
}
function rmrf (dir) { try { fs.rmSync(dir, { recursive: true, force: true }) } catch {} }
function mktemp (prefix) {
const dir = path.join(os.tmpdir(), 'keet-daemon-test-' + prefix + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
fs.mkdirSync(dir, { recursive: true })
return dir
}
// Random gibberish name: 8 lowercase letters + 1 digit. Designed so no real
// human would ever pick it as a handle.
function randomGibberishName () {
const alpha = 'abcdefghijklmnopqrstuvwxyz'
let s = ''
for (let i = 0; i < 8; i++) s += alpha[Math.floor(Math.random() * 26)]
return s + Math.floor(Math.random() * 10)
}
class Client {
constructor (socketPath) {
this._socketPath = socketPath
this._pipe = null
this._framed = null
this._nextId = 1
this._pending = new Map()
}
connect () {
return new Promise((resolve, reject) => {
const pipe = new Pipe(this._socketPath)
const onErr = (err) => reject(err)
pipe.once('error', onErr)
pipe.once('connect', () => {
pipe.removeListener('error', onErr)
this._pipe = pipe
const FS = FramedStream.default || FramedStream
this._framed = new FS(pipe)
this._framed.on('data', (buf) => {
let msg
try { msg = JSON.parse(buf.toString('utf-8')) } catch { return }
const p = this._pending.get(msg.id)
if (!p) return
this._pending.delete(msg.id)
if (msg.ok === false || msg.error) p.reject(new Error(msg.error || 'RPC failed'))
else p.resolve(msg.data !== undefined ? msg.data : msg)
})
resolve()
})
})
}
call (method, params = {}, timeoutMs = 30000) {
const id = this._nextId++
const payload = JSON.stringify({ id, method, params })
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._pending.delete(id)
reject(new Error('RPC timeout: ' + method))
}, timeoutMs)
this._pending.set(id, {
resolve: (v) => { clearTimeout(timer); resolve(v) },
reject: (e) => { clearTimeout(timer); reject(e) }
})
this._framed.write(Buffer.from(payload, 'utf-8'))
})
}
close () {
try { this._framed && this._framed.destroy() } catch {}
try { this._pipe && this._pipe.destroy() } catch {}
}
}
async function main () {
const baseDir = mktemp('bootstrap-full')
const socketPath = path.join(baseDir, 'daemon.sock')
console.log('[test] baseDir = ' + baseDir)
let daemon
try {
daemon = await startDaemon({ storage: baseDir, socketPath })
ok(true, 'startDaemon resolved')
} catch (err) {
ok(false, 'startDaemon: ' + err.message); rmrf(baseDir); return
}
const client = new Client(socketPath)
try { await client.connect(); ok(true, 'UDS connected') }
catch (err) { ok(false, 'UDS connect: ' + err.message); rmrf(baseDir); return }
// 1. List
try {
const list = await client.call('identity-manager.list', {}, 10000)
ok(list.activeId === 'identity-0', 'activeId == identity-0')
ok(list.identities.length === 1, '1 identity present')
} catch (err) { ok(false, 'list: ' + err.message) }
// 2. Create + switch
let newId
try {
const created = await client.call('identity-manager.create', { name: 'bootstrap-full-' + Date.now().toString(36) }, 15000)
newId = created.id || (created.entry && created.entry.id)
ok(newId === 'identity-1', 'create returned identity-1')
} catch (err) { ok(false, 'create: ' + err.message) }
if (newId) {
try {
const r = await client.call('identity-manager.switch', { id: newId }, 30000)
ok(r.ok === true, 'switch ok')
} catch (err) { ok(false, 'switch: ' + err.message) }
}
// 3. vault.setup — mnemonic backed up, entropy wiped
const password = 'test-pw-' + Math.random().toString(36).slice(2, 10)
try {
const r = await client.call('vault.setup', { password }, 90000)
ok(r.ok === true, 'vault.setup ok')
} catch (err) { ok(false, 'vault.setup: ' + err.message) }
// 4. mnemonic.enc must exist on disk
if (newId) {
const vaultFile = path.join(baseDir, newId, 'mnemonic.enc')
ok(fs.existsSync(vaultFile), 'mnemonic.enc written at ' + vaultFile)
}
// 5. setName — display name persists in profile
const displayName = 'test-' + randomGibberishName().slice(0, 4)
try {
const r = await client.call('identity.setName', { name: displayName }, 15000)
ok(r.ok === true, 'identity.setName ok (' + displayName + ')')
} catch (err) { ok(false, 'identity.setName: ' + err.message) }
// 6. checkName — read-only, no global registry claim
const candidate = randomGibberishName()
console.log('[test] random candidate for checkName: ' + candidate)
try {
const r = await client.call('identity.checkName', { username: candidate }, 15000)
ok(typeof r.available === 'boolean', 'checkName returned available=' + r.available)
} catch (err) {
// checkName may fail if the userRegistry isn't reachable in the test env;
// not a hard failure here — we're testing the daemon wiring, not the
// global registry reachability.
console.log('[test] checkName error (non-fatal in test env): ' + err.message)
}
// 7. Final identity shape
try {
const id = await client.call('identity', {}, 15000)
ok(id && !!id.publicKey, 'identity.publicKey populated')
ok(id && id.displayName === displayName, 'identity.displayName == ' + displayName + ' (got ' + (id && id.displayName) + ')')
} catch (err) { ok(false, 'identity: ' + err.message) }
// Cleanup
console.log('[test] cleanup')
client.close()
try { await daemon.router.core.close() } catch {}
try { await daemon.core.close() } catch {}
try { await daemon.uds.stop() } catch {}
rmrf(baseDir)
console.log('')
if (exitCode === 0) console.log('=== RESULT: ALL PASSED ===')
else {
console.log('=== RESULT: ' + failures.length + ' FAILED ===')
for (const f of failures) console.log(' - ' + f)
}
Bare.exit(exitCode)
}
main().catch(err => {
console.error('[test] uncaught:', err && err.message)
if (err && err.stack) console.error(err.stack)
Bare.exit(1)
})