// name.test.js — what a record is called.
//
// ⛔ THE DEFECT THIS CLOSES. A record's name was `String(Date.now())` (rpc.js post and reply) or a
// clock plus four hex characters (room.js). Two things were wrong with that, and hyper-social #59 §2
// named both: a millisecond clock COLLIDES, and the name was `a.name || …` — caller-supplied — so a
// collision was CHOOSABLE, not merely improbable. Apply then wrote it with an unconditional `set`,
// so a collision silently overwrote a record and, in a room, silently reattributed it.
//
// ⛔ AND IT IS NOT A POSITION. hyper-social #61 §2b measured keet and found the reverse of what #59
// had recorded: `id: {thread, index}` is a view coordinate, and every cross-reference in keet
// resolves through `@keet/chat-by-message-id` keyed by `oplog` (chat.js:37-43, and the EDIT target
// at :89-101). A position is a function of the linearisation, and the linearisation is re-derived on
// every boot and every reorg — so a permalink built from one can change after it has been handed
// out. A name must be settled the moment the record exists and never move again.
//
// So: minted at write time, unguessable enough that collision is not a practical event, ordered by
// time because timelines and every bridge's rkey want that, and NEVER supplied by a caller.
import test from 'brittle'
import { recordName, nameTime, isRecordName, NAME_LEN } from '../lib/name.js'
test('★★ two names minted in the SAME millisecond differ', async (t) => {
const at = 1_788_000_000_000
const seen = new Set()
for (let i = 0; i < 10_000; i++) seen.add(recordName(at))
t.is(seen.size, 10_000, '10k names, one millisecond, zero collisions')
})
// The old shape, for contrast: this is what String(Date.now()) gave you.
test('⛔ the OLD scheme collides on the first duplicate millisecond', async (t) => {
const at = 1_788_000_000_000
t.is(String(at), String(at), 'two posts in one millisecond were literally the same name')
})
test('★★ lexicographic order IS chronological order', async (t) => {
const early = recordName(1_788_000_000_000)
const mid = recordName(1_788_000_000_001)
const late = recordName(1_799_000_000_000)
t.ok(early < mid, 'one millisecond later sorts later')
t.ok(mid < late, 'and much later sorts later still')
// A whole run stays sorted — the property a timeline and a TID projection both depend on.
const names = []
for (let i = 0; i < 200; i++) names.push(recordName(1_788_000_000_000 + i))
t.alike(names, [...names].sort(), 'minting order and sort order agree')
})
test('★ fixed width, one path segment, no separator to escape', async (t) => {
const n = recordName()
t.is(n.length, NAME_LEN)
t.ok(/^[0-9a-v]+$/.test(n), 'base32hex, lowercase — sortable by ASCII and safe in a path or a URL')
t.absent(n.includes('/'))
t.absent(n.includes('.'))
})
test('★ a name carries the moment it was minted', async (t) => {
const at = 1_788_123_456_789
t.is(nameTime(recordName(at)), at, 'and decodes back exactly')
})
test('★ isRecordName accepts what we mint and rejects what we do not', async (t) => {
t.ok(isRecordName(recordName()))
t.absent(isRecordName('1788822724600'), 'a legacy clock name is NOT this format')
t.absent(isRecordName('self'))
t.absent(isRecordName(''))
t.absent(isRecordName(null))
t.absent(isRecordName(recordName() + 'x'), 'wrong length')
t.absent(isRecordName('W'.repeat(NAME_LEN)), 'outside the alphabet')
})
// ⛔ Legacy names must keep working. Records written before this change are on disk on three
// machines, and `record-store.js` treats a name as an opaque path segment — so the two formats
// coexist and nothing needs migrating. This test exists so nobody later "tidies up" by rejecting
// them.
test('⛔ legacy clock names remain valid record names — no migration', async (t) => {
t.is(nameTime('1788822724600'), null, 'we cannot decode one, and say so rather than guessing')
t.absent(isRecordName('1788822724600'), 'it is not the new format')
// What matters is that nothing here refuses it as a KEY; that is record-store.js's isSafeSeg,
// which asks only that it be a single non-dot path segment.
t.absent('1788822724600'.includes('/'), 'and it still satisfies that')
})