// Standalone identity-switch probe — exercises the path hyper-tps actually
// fails on: identity-manager.create → identity-manager.switch → vault.setup.
// The sister test identity-bootstrap.test.js only exercises vault.setup on
// the auto-migrated identity-0; it does NOT go through swapCore() and so
// can't catch the destroyAllSubscriptions / core.close cascade.
//
// 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-switch.test.js
//
// Exit code 0 = success, 1 = any failure (with diagnostics printed).
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
}
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('switch')
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 — this test is fully isolated)')
let daemon
const t0 = Date.now()
try {
daemon = await startDaemon({ storage: baseDir, socketPath })
ok(true, 'startDaemon resolved in ' + (Date.now() - t0) + 'ms')
} catch (err) {
ok(false, 'startDaemon threw: ' + err.message)
if (err.stack) console.log(err.stack)
rmrf(baseDir)
return
}
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)
return
}
// 1. List identities — fresh install path should yield identity-0
let initialList
try {
initialList = await client.call('identity-manager.list', {}, 10000)
ok(initialList && Array.isArray(initialList.identities), 'list.identities is an array')
ok(initialList.activeId === 'identity-0', 'activeId === identity-0; got ' + initialList.activeId)
ok(initialList.identities.length === 1, 'one identity present; got ' + (initialList.identities && initialList.identities.length))
} catch (err) {
ok(false, 'identity-manager.list threw: ' + err.message)
}
// 2. identity-manager.create — allocates identity-1
let newId
const tCreate = Date.now()
try {
const created = await client.call('identity-manager.create', { name: 'test-switch-' + Date.now().toString(36) }, 15000)
const elapsed = Date.now() - tCreate
newId = (created && created.id) || (created && created.entry && created.entry.id)
ok(!!newId, 'identity-manager.create returned id in ' + elapsed + 'ms; got ' + (newId || JSON.stringify(created)))
if (newId) {
ok(newId === 'identity-1', 'new id is identity-1; got ' + newId)
ok(fs.existsSync(path.join(baseDir, newId)), 'new identity dir created on disk: ' + newId)
}
} catch (err) {
ok(false, 'identity-manager.create threw in ' + (Date.now() - tCreate) + 'ms: ' + err.message)
if (err.stack) console.log(err.stack)
}
// 3. identity-manager.switch — THE PATH UNDER TEST
// Goes through _imSwitch → swapCore → uds.destroyAllSubscriptions
// → await core.close() → KeetCore.from(new storage) → backend.ready()
// → router.swapMetadata. If anything crashes, this is the call that
// surfaces it as `{ ok:false, error: ... }`.
if (newId) {
const tSwitch = Date.now()
try {
const result = await client.call('identity-manager.switch', { id: newId }, 30000)
const elapsed = Date.now() - tSwitch
ok(result && result.ok === true, 'identity-manager.switch returned {ok:true} in ' + elapsed + 'ms')
ok(result && result.active && result.active.id === newId, 'switch result.active.id === ' + newId)
} catch (err) {
ok(false, 'identity-manager.switch threw in ' + (Date.now() - tSwitch) + 'ms: ' + err.message)
if (err.stack) console.log(err.stack)
}
}
// 4. Confirm registry reflects the swap
if (newId) {
try {
const afterList = await client.call('identity-manager.list', {}, 10000)
ok(afterList && afterList.activeId === newId, 'post-switch activeId === ' + newId + '; got ' + (afterList && afterList.activeId))
ok(afterList && afterList.identities && afterList.identities.length === 2, 'two identities present; got ' + (afterList && afterList.identities && afterList.identities.length))
} catch (err) {
ok(false, 'post-switch identity-manager.list threw: ' + err.message)
}
}
// 5. vault.setup on the freshly-switched-to identity
if (newId) {
const password = 'test-pw-' + Math.random().toString(36).slice(2, 10)
const tVault = Date.now()
try {
const setupResult = await client.call('vault.setup', { password }, 90000)
const elapsed = Date.now() - tVault
ok(true, 'vault.setup OK in ' + elapsed + 'ms')
ok(setupResult && setupResult.ok === true, 'vault.setup returned {ok:true}; got ' + JSON.stringify(setupResult))
} catch (err) {
ok(false, 'vault.setup failed in ' + (Date.now() - tVault) + 'ms: ' + err.message)
if (err.stack) console.log(err.stack)
}
// 6. identity RPC should now return a publicKey
try {
const id = await client.call('identity', {}, 10000)
ok(id && id.publicKey, 'identity.publicKey present after vault.setup: ' + (id && id.publicKey ? id.publicKey.slice(0, 16) + '…' : '<missing>'))
} catch (err) {
ok(false, 'identity RPC failed: ' + err.message)
}
// 7. Disk evidence on the NEW identity
const idDir = path.join(baseDir, newId)
ok(fs.existsSync(idDir), newId + ' dir exists on disk')
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')
}
}
// Cleanup. After swapCore, daemon.core points to the ORIGINAL (closed) core;
// the live one is on daemon.router.core. Try both, swallow.
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')
else {
console.log(failures.length + ' FAILED:')
for (const f of failures) console.log(' - ' + f)
}
process.exit(exitCode)
}
main().catch(err => {
console.error('uncaught:', err)
if (err && err.stack) console.error(err.stack)
process.exit(1)
})