// The namespaces this node holds, and which slot each one's log lives in.
//
// ⛔ `namespaceSettings` has always told people "the one you are in now stays on disk — you can come
// back to it, handles and all". That was true and unreachable: nothing listed what was on disk, so
// coming back meant having written down a 64-hex key. An index is the difference between a promise
// and a door.
import test from 'brittle'
import process from '#node:process'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import { loadNamespaces, saveNamespaces, slotFor, namespaceSlot, saveNamespace, loadNamespace } from '../lib/device.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-ns-'))
const A = 'a'.repeat(64)
const B = 'b'.repeat(64)
test('the index round-trips, and drops anything malformed', async (t) => {
process.env.FAVELA_NAMESPACE = ''
const d = tmp()
saveNamespaces(d, [{ key: A, slot: 'names' }, { key: 'nope', slot: 'x' }, { key: B, slot: 'names:' + B }])
const got = loadNamespaces(d)
t.absent(got.some((n) => n.key === 'nope'), 'the malformed key is not kept')
t.ok(got.some((n) => n.key === A) && got.some((n) => n.key === B), 'both valid ones survive')
// ⚠ The CURRENT namespace is always included, and a fresh node now has one — the default. So this
// asserts membership rather than a count, which would otherwise track an unrelated decision.
t.is(got.filter((n) => n.key === A || n.key === B).length, 2)
})
test('⛔ the CURRENT namespace is always listed, even if the index never mentioned it', async (t) => {
const d = tmp()
saveNamespaces(d, [{ key: B, slot: 'names:' + B }])
saveNamespace(d, { key: A, slot: 'names' })
const got = loadNamespaces(d)
// An index that could omit the namespace you are standing in would offer a switch to nowhere.
t.ok(got.some((n) => n.key === A), 'the one this node is in appears')
t.is(got.length, 2)
})
test('★ returning to the FOUNDED namespace reuses its legacy slot, not a derived one', async (t) => {
// This is the bug. The founded log lives in `names`; namespaceSlot() cannot produce that, so
// leaving and returning used to open a SECOND log and leave the claims behind in the first.
const known = [{ key: A, slot: 'names' }]
const current = { key: B, slot: 'names:' + B }
t.is(slotFor(A, { current, known }), 'names', 'the recorded slot wins over the derived one')
t.not(slotFor(A, { current, known }), namespaceSlot(A), 'and it is NOT what would have been derived')
})
test('a namespace we are standing in knows its own slot', async (t) => {
const current = { key: A, slot: 'names' }
t.is(slotFor(A, { current, known: [] }), 'names')
})
test('an unknown namespace derives a fresh slot', async (t) => {
t.is(slotFor(B, { current: null, known: [] }), namespaceSlot(B))
// ⚠ Never the legacy `names`: that slot belongs to whatever this node founded, and opening a
// stranger's log there would put two namespaces in one Corestore slot — the deadlock case.
t.not(slotFor(B, { current: null, known: [] }), 'names')
})
// ⛔ A fresh node used to FOUND ITS OWN namespace and resolve handles in a world containing only
// itself — so a new device could join the fleet, hold the right feeds, and still find nobody by
// name. That is what a handset did, and it read as a broken network rather than an empty registry.
test('★ a fresh node defaults to the fleet namespace instead of founding its own', async (t) => {
// ⚠ Cleared explicitly: another file sets FAVELA_NAMESPACE=own and env is process-wide, so a test
// that does not say what it wants inherits whatever ran before it.
process.env.FAVELA_NAMESPACE = ''
const d = tmp()
const ns = loadNamespace(d)
t.ok(ns && /^[0-9a-f]{64}$/.test(ns.key), 'there is a default at all')
// ⚠ NOT the legacy `names` slot: that belongs to a namespace this node founded, and opening
// someone else's log there would put two namespaces in one Corestore slot — the deadlock case.
t.not(ns.slot, 'names', 'it joins, and joining gets its own slot')
t.is(ns.slot, namespaceSlot(ns.key))
})
// ⚠ NAMING.md §8 #2: "a default that cannot be changed is a root". The override is what keeps this
// a convenient starting point rather than an authority.
test('⛔ FAVELA_NAMESPACE overrides the default', async (t) => {
const d = tmp()
const mine = 'c'.repeat(64)
process.env.FAVELA_NAMESPACE = mine
t.is(loadNamespace(d).key, mine, 'the configured one wins')
process.env.FAVELA_NAMESPACE = ''
t.not(loadNamespace(d).key, mine, 'and clearing it falls back')
})
// ⛔ A node that already has one is NOT moved. Changing where an existing install resolves handles
// would strand every name it claimed, silently.
test('⛔ a node with a stored namespace keeps it', async (t) => {
process.env.FAVELA_NAMESPACE = ''
const d = tmp()
saveNamespace(d, { key: 'd'.repeat(64), slot: 'names' })
t.is(loadNamespace(d).key, 'd'.repeat(64))
t.is(loadNamespace(d).slot, 'names', 'including the founded slot')
})