// Proof test: identity-manager.import (which seeds identity bytes via
// restoreIdentity BEFORE awaiting onlineBoot-dependent RPCs) works on a
// fresh storage where identity-manager.switch + vault.setup hangs at 90s.
//
// If this test passes while test/identity-switch.test.js fails on the same
// daemon, the diagnosis is confirmed: vault.setup on a freshly-swapped-to
// identity blocks on onlineBoot.ready() that never resolves because
// _loadIdentity errors with "Identity is required". The fix path for
// hyper-tps's `--fresh` mode is to call identity-manager.import with a
// locally-generated mnemonic instead of identity-manager.create + .switch
// + vault.setup.
//
// SAFETY: storage is a fresh tmp dir under os.tmpdir(). The host's
// ~/.config/keet-cli/ is NEVER touched. Cleanup is unconditional.
//
// Run:
// cd hyper-fleeky/packages/keet-daemon && bare test/identity-import-proves-fix.test.js
//
// Exit code 0 = success (fix path confirmed), 1 = any failure.
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 { generateMnemonic } from 'bip39-mnemonic'
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
}
class TestClient {
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 = 60000) {
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 + 'ms)'))
}, 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('import-proves-fix')
const socketPath = path.join(baseDir, 'daemon.sock')
console.log('[test] baseDir = ' + baseDir)
console.log('[test] socket = ' + socketPath)
console.log('[test] (host ~/.config/keet-cli is NOT used — fully isolated)')
let daemon
const tBoot = Date.now()
try {
daemon = await startDaemon({ storage: baseDir, socketPath })
ok(true, 'startDaemon resolved in ' + (Date.now() - tBoot) + 'ms')
} catch (err) {
ok(false, 'startDaemon threw: ' + err.message)
if (err.stack) console.log(err.stack)
rmrf(baseDir)
Bare.exit(1)
}
const client = new TestClient(socketPath)
try {
await client.connect()
ok(true, 'UDS client connected')
} catch (err) {
ok(false, 'UDS connect failed: ' + err.message)
try { await daemon.core.close() } catch {}
rmrf(baseDir)
Bare.exit(1)
}
// 1. Sanity: identity-0 was created by registry migration
try {
const list = await client.call('identity-manager.list', {}, 10000)
ok(list && list.activeId === 'identity-0', 'pre-import activeId === identity-0; got ' + (list && list.activeId))
ok(list && list.identities && list.identities.length === 1, 'one identity present; got ' + (list && list.identities && list.identities.length))
} catch (err) {
ok(false, 'identity-manager.list threw: ' + err.message)
}
// 2. Generate a fresh mnemonic locally. This is what hyper-tps would do
// in `--fresh` mode if it called identity-manager.import instead of
// create + switch + vault.setup.
const phrase = generateMnemonic()
const password = 'proof-pw-' + Math.random().toString(36).slice(2, 10)
const name = 'fix-proof-' + Date.now().toString(36)
ok(typeof phrase === 'string' && phrase.split(' ').length >= 12, 'generated 12+ word mnemonic locally')
// 3. identity-manager.import — the path that does:
// registry.create → swapCore → restoreIdentity(phrase) → vault.encrypt
// restoreIdentity SEEDS the identity bytes inside the new core so
// onlineBoot.ready() can resolve afterward.
//
// NOTE: identity-manager.import uses 90s as its operational ceiling
// (vault.setup's timeout in identity-switch.test.js). We use the
// same here so a hang would manifest identically — proving the
// diagnosis by contrast.
let importResult
const tImport = Date.now()
try {
importResult = await client.call('identity-manager.import', { phrase, password, name }, 90000)
const elapsed = Date.now() - tImport
ok(true, 'identity-manager.import returned in ' + elapsed + 'ms (vs vault.setup hang at 90000ms)')
ok(importResult && importResult.ok === true, 'import returned {ok:true}; got ' + JSON.stringify(importResult))
ok(importResult && importResult.identity && importResult.identity.id, 'import returned identity entry; got id=' + (importResult && importResult.identity && importResult.identity.id))
} catch (err) {
ok(false, 'identity-manager.import failed in ' + (Date.now() - tImport) + 'ms: ' + err.message)
if (err.stack) console.log(err.stack)
}
const newId = importResult && importResult.identity && importResult.identity.id
// 4. Post-import list — active should be the new id
if (newId) {
try {
const list = await client.call('identity-manager.list', {}, 10000)
ok(list && list.activeId === newId, 'post-import activeId === ' + newId + '; got ' + (list && list.activeId))
ok(list && list.identities && list.identities.length === 2, 'two identities present; got ' + (list && list.identities && list.identities.length))
} catch (err) {
ok(false, 'post-import identity-manager.list threw: ' + err.message)
}
}
// 5. The decisive check: `identity` RPC returns a publicKey WITHOUT
// hanging. In identity-switch.test.js this exact call timed out at
// 10s because onlineBoot never resolved. Here, because we seeded
// identity bytes via restoreIdentity, it should return quickly.
if (newId) {
const tId = Date.now()
try {
const id = await client.call('identity', {}, 15000)
const elapsed = Date.now() - tId
ok(id && id.publicKey, 'identity RPC returned publicKey in ' + elapsed + 'ms (vs 10000ms hang after vault.setup); got ' + (id && id.publicKey ? id.publicKey.slice(0, 16) + '…' : '<missing>'))
} catch (err) {
ok(false, 'identity RPC failed in ' + (Date.now() - tId) + 'ms: ' + err.message)
}
}
// 6. Disk evidence — vault file + identity db both present
if (newId) {
const idDir = path.join(baseDir, newId)
ok(fs.existsSync(idDir), newId + ' dir exists on disk')
const vaultFile = path.join(idDir, 'mnemonic.enc')
ok(fs.existsSync(vaultFile), 'vault file mnemonic.enc exists (vault.encrypt ran)')
const dbDir = path.join(idDir, '0', 'db')
if (fs.existsSync(dbDir)) {
const entries = fs.readdirSync(dbDir)
ok(entries.length > 0, newId + '/0/db has ' + entries.length + ' files')
} else {
ok(false, newId + '/0/db does not exist')
}
}
// 7. Round-trip: vault.export with the password should yield the original phrase.
// This proves the vault encrypted what we passed AND the core's identity
// matches it.
if (newId) {
try {
const exported = await client.call('vault.export', { password }, 10000)
ok(exported && exported.phrase === phrase, 'vault.export round-trips the original mnemonic; ' + (exported && exported.phrase === phrase ? 'match' : 'MISMATCH'))
} catch (err) {
ok(false, 'vault.export failed: ' + err.message)
}
}
// Cleanup — daemon.core was swapped; use daemon.router.core for the live one
console.log('[test] cleaning up…')
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('')
console.log('=== RESULT ===')
if (exitCode === 0) {
console.log('ALL PASSED — fix path (restoreIdentity-then-vault) confirmed working on fresh storage.')
console.log('hyper-tps --fresh should call identity-manager.import { phrase: generateMnemonic(), password, name }')
console.log('instead of identity-manager.create + .switch + vault.setup.')
} else {
console.log(failures.length + ' FAILED:')
for (const f of failures) console.log(' - ' + f)
}
Bare.exit(exitCode)
}
main().catch(err => {
console.error('uncaught:', err)
if (err && err.stack) console.error(err.stack)
Bare.exit(1)
})