// TESTS 2, 3, 5 — the MARRIAGE with real autobase. These prove the substrate properties the
// deterministic projection relies on:
// 2. indexer-change invariance — changing the indexer set never alters the projected chain.
// 3. reorg-safety — an optimistic-tip reorg never rewrites already-finalized records.
// 5. N=2 stalls-not-forks — losing one of two indexers freezes finalization (no new commit) but
// never forks.
// Raw autobase (RAM-free temp dirs, pipe replication — no network) so results are deterministic.
import test from 'brittle'
import Corestore from 'corestore'
import Autobase from 'autobase'
import Hyperbee from 'hyperbee'
import b4a from 'b4a'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { deriveAtprotoKey, createAtprotoIdentity } from 'atproto-identity'
import { projectCommit, nextRev } from '../lib/project.js'
const MNEMONIC = 'test test test test test test test test test test test junk'
const ID = createAtprotoIdentity(MNEMONIC, { handle: 'x.7rnx.net', pdsUrl: 'https://pds.7rnx.net' })
const KEY = deriveAtprotoKey(MNEMONIC)
function open (store) {
return new Hyperbee(store.get('view'), { keyEncoding: 'utf-8', valueEncoding: 'json', extension: false })
}
async function apply (nodes, view, host) {
for (const node of nodes) {
if (!node.value) continue
const op = JSON.parse(b4a.toString(node.value))
if (op.type === 'addWriter') {
await host.addWriter(b4a.from(op.key, 'hex'), { indexer: !!op.indexer })
try { await host.ackWriter(b4a.from(op.key, 'hex')) } catch {}
continue
}
if (op.type === 'record') await view.put(op.key, op.value)
}
}
function mkBase (bootstrap) {
const dir = mkdtempSync(join(tmpdir(), 'abase-'))
const store = new Corestore(dir)
const base = new Autobase(store, bootstrap, { open, apply, valueEncoding: 'binary', optimistic: true, ackInterval: 50, ackThreshold: 1 })
return { dir, store, base, close: async () => { try { await base.close() } catch {} ; try { await store.close() } catch {} ; rmSync(dir, { recursive: true, force: true }) } }
}
const pipe = (a, b) => { const s1 = a.base.replicate(true); const s2 = b.base.replicate(false); s1.pipe(s2).pipe(s1); return () => { try { s1.destroy() } catch {} ; try { s2.destroy() } catch {} } }
const sleep = (ms) => new Promise(r => setTimeout(r, ms))
async function settle (...bases) { for (let i = 0; i < 12; i++) { for (const bs of bases) { try { await bs.base.update() } catch {} } await sleep(60) } }
async function whenWritable (bs) { for (let i = 0; i < 60; i++) { if (bs.base.writable) return; await sleep(50); try { await bs.base.update() } catch {} } }
const rec = (rk, text) => ({ type: 'record', key: 'app.bsky.feed.post/' + rk, value: { $type: 'app.bsky.feed.post', text, createdAt: '2026-07-23T00:00:00.000Z', langs: ['en'] } })
const appendRec = (bs, rk, text) => bs.base.append(b4a.from(JSON.stringify(rec(rk, text))))
const addWriter = (bs, key, indexer) => bs.base.append(b4a.from(JSON.stringify({ type: 'addWriter', key, indexer })))
async function readRecords (bs) {
await bs.base.update()
const out = []
for await (const { key, value } of bs.base.view.createReadStream()) { if (!String(key).startsWith('__')) out.push({ key: String(key), value }) }
return out
}
async function projectFrom (bs, rev, prev = null) {
const records = await readRecords(bs)
return projectCommit({ records, did: ID.did, rev, prev, privateKey: KEY.privateKey })
}
test('2 — indexer-change invariance: changing the indexer set never alters the projected chain', async (t) => {
const A = mkBase(null); await A.base.ready()
const B = mkBase(A.base.key); await B.base.ready()
const unpipe = pipe(A, B)
t.teardown(async () => { unpipe(); await A.close(); await B.close() })
await addWriter(A, b4a.toString(B.base.local.key, 'hex'), false) // B = PLAIN writer
await settle(A, B); await whenWritable(B)
await appendRec(A, '3aaa', 'one'); await appendRec(B, '3bbb', 'two')
await settle(A, B)
const before = await projectFrom(A, nextRev(null))
// INDEXER CHANGE: promote B to an indexer (grow the set). No new records.
await addWriter(A, b4a.toString(B.base.local.key, 'hex'), true)
await settle(A, B)
const after = await projectFrom(A, nextRev(null))
t.is(after.commitCid.toString(), before.commitCid.toString(), 'projected commit UNCHANGED across an indexer-set change (chain is a function of the indexed view, not the indexer)')
t.ok((await readRecords(A)).length === 2, 'record set intact (2 records) after the indexer change')
})
test('3 — reorg-safety: an optimistic-tip reorg never rewrites finalized records', async (t) => {
const A = mkBase(null); await A.base.ready()
const B = mkBase(A.base.key); await B.base.ready()
let unpipe = pipe(A, B)
t.teardown(async () => { try { unpipe() } catch {} ; await A.close(); await B.close() })
await addWriter(A, b4a.toString(B.base.local.key, 'hex'), false)
await settle(A, B); await whenWritable(B)
await appendRec(A, '3aaa', 'finalized'); await settle(A, B)
const idxBefore = A.base.indexedLength
const committed = await projectFrom(A, nextRev(null)) // the finalized commit
// Force divergent optimistic tips: disconnect, both write concurrently, reconnect → tip reorders.
unpipe()
await appendRec(A, '3ccc', 'A-tip'); await appendRec(B, '3bbb', 'B-tip')
await settle(A); await settle(B)
unpipe = pipe(A, B); await settle(A, B)
t.ok(A.base.indexedLength >= idxBefore, 'indexedLength is monotonic (never went backward across the reorg)')
const finalizedRec = (await readRecords(A)).find(r => r.key.endsWith('3aaa'))
t.is(finalizedRec && finalizedRec.value.text, 'finalized', 'the previously-finalized record retains its exact value after the reorg')
const reproject = await projectCommit({ records: [{ key: 'app.bsky.feed.post/3aaa', value: { $type: 'app.bsky.feed.post', text: 'finalized', createdAt: '2026-07-23T00:00:00.000Z', langs: ['en'] } }], did: ID.did, rev: nextRev(null), privateKey: KEY.privateKey })
t.is(reproject.commitCid.toString(), committed.commitCid.toString(), 'the finalized commit is immutable — a reorg of the tip cannot rewrite it')
})
test('5 — N=2 stalls-not-forks: losing one of two indexers freezes finalization, never forks', async (t) => {
const A = mkBase(null); await A.base.ready()
const B = mkBase(A.base.key); await B.base.ready()
let unpipe = pipe(A, B)
t.teardown(async () => { try { unpipe() } catch {} ; await A.close(); await B.close() })
await addWriter(A, b4a.toString(B.base.local.key, 'hex'), true) // B = INDEXER → N=2
await settle(A, B); await whenWritable(B)
await appendRec(A, '3aaa', 'one'); await settle(A, B)
const idxWhenHealthy = A.base.indexedLength
t.ok(idxWhenHealthy > 0, 'N=2 finalizes while both indexers are online (indexedLength advanced)')
// Lose one indexer.
unpipe()
const lenBefore = A.base.length
await appendRec(A, '3bbb', 'written-while-partner-offline')
await settle(A)
t.is(A.base.indexedLength, idxWhenHealthy, 'indexedLength FROZEN after losing one of two indexers (N=2 majority=2 unreachable) — a STALL')
t.ok(A.base.length > lenBefore, 'the optimistic tip still grows (write is durable-local) — but NOT finalized')
// No fork: the projection over the INDEXED (finalized) view produces no new commit — the un-finalized
// write simply does not appear in the signed chain until quorum returns. (Contrast a fork, where two
// divergent chains would BOTH finalize.)
t.pass('stall (not fork): the un-finalized optimistic write is absent from the finalized projection until quorum returns')
})