// createUser must never report success for a write that did not land.
//
// Current behaviour (identity-registry.js:133-138):
// await this._base.append(encodeOp(op))
// await this._base.update()
// const user = await this.getUser(userId)
// return user || op // <-- fabricates a user from the in-memory op
//
// `op` carries a userId minted locally at :111, so the caller receives an
// object indistinguishable from a persisted user. In production this turned a
// peer that could not linearize into a silent data-loss bug: every request
// re-ran TOFU, got a fresh fabricated userId, and the profile page rendered
// "Not authenticated yet" forever — with no error anywhere.
//
// The rule this restores is the one every durable autobase app follows
// (autobase skill, admission-patterns.md): "if admission can't be reached,
// THROW — never report success on a write that won't commit."
import test from 'brittle'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import path from '#node:path'
import os from '#node:os'
import crypto from 'hypercore-crypto'
import { IdentityRegistry } from '../lib/auth/identity-registry.js'
async function createRegistry () {
const dir = path.join(os.tmpdir(), 'hyper-identity-nofab-' + crypto.randomBytes(8).toString('hex'))
const store = new Corestore(dir)
await store.ready()
const db = new Hyperbee(store.get({ name: 'test-db' }), {
keyEncoding: 'utf-8', valueEncoding: 'json'
})
await db.ready()
const registry = new IdentityRegistry(store, { db })
await registry.ready()
return {
registry,
cleanup: async () => { await registry.close(); await db.close(); await store.close() }
}
}
test('createUser returns the persisted user on the happy path', async (t) => {
const { registry, cleanup } = await createRegistry()
const user = await registry.createUser({ name: 'Alice' })
t.ok(user && user.userId, 'returns a user with an id')
t.is(user.name, 'Alice')
const readBack = await registry.getUser(user.userId)
t.ok(readBack, 'the user is genuinely readable from the view')
t.is(readBack.userId, user.userId, 'the returned id matches what persisted')
await cleanup()
})
test('createUser THROWS when the write does not land — no fabricated user', async (t) => {
const { registry, cleanup } = await createRegistry()
// Simulate a peer whose appends cannot linearize (stortron's condition:
// admitted and "writable", but the linearizer has no indexers so nothing
// ever confirms). The append resolves; the record never appears.
registry.base.append = async () => {}
await t.exception(
() => registry.createUser({ name: 'ghost' }),
/did not land|not persist/i,
'createUser throws instead of returning a user that was never written'
)
await cleanup()
})
test('a failed createUser leaves nothing behind and is not silently retried into existence', async (t) => {
const { registry, cleanup } = await createRegistry()
registry.base.append = async () => {}
let firstErr = null
try { await registry.createUser({ name: 'ghost-1' }) } catch (err) { firstErr = err }
t.ok(firstErr, 'first attempt threw')
// The listing must not contain a phantom. Restore append so listUsers works
// against a functioning view.
const users = await registry.listUsers()
t.absent(users.some(u => u.name === 'ghost-1'), 'no phantom user was persisted')
await cleanup()
})