#!/usr/bin/env bare
/**
* Edge case tests for Autobase multi-writer over Hyperswarm.
* These cover scenarios that could break in production.
*/
import test from 'brittle'
import Corestore from 'corestore'
import Autobase from 'autobase'
import Hyperbee from 'hyperbee'
import Hyperswarm from 'hyperswarm'
import createTestnet from '@hyperswarm/testnet'
import os from 'bare-os'
import path from 'bare-path'
import fs from 'bare-fs'
const tmpdir = os.tmpdir()
let idx = 0
function tmpPath (name) {
return path.join(tmpdir, 'ab-edge-' + name + '-' + (idx++) + '-' + Date.now())
}
async function cleanup (...dirs) {
for (const d of dirs) await fs.promises.rm(d, { recursive: true }).catch(() => {})
}
function open (store) {
return new Hyperbee(store.get('view', { valueEncoding: 'json' }), {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
}
// Mimics pear-git LinkedRepo apply — with probe validation
async function peerGitApply (batch, view, host) {
const b = view.batch()
for (const node of batch) {
const op = node.value
if (!op || !op.type) continue
switch (op.type) {
case 'create-issue': {
await b.put('issue:' + op.id, {
id: op.id,
title: op.title,
body: op.body || '',
author: op.author || 'unknown',
status: 'open',
createdAt: op.timestamp || Date.now()
})
const counter = await b.get('counter:issues')
const next = counter ? counter.value.next : 0
if (op.id >= next) await b.put('counter:issues', { next: op.id + 1 })
break
}
case 'update-issue': {
const entry = await b.get('issue:' + op.id)
if (entry) {
const issue = entry.value
if (op.status) issue.status = op.status
if (op.title) issue.title = op.title
await b.put('issue:' + op.id, issue)
}
break
}
case 'add-writer': {
if (op.key) {
try {
await host.addWriter(Buffer.from(op.key, 'hex'), { indexer: op.indexer !== false })
} catch {}
}
break
}
}
}
await b.flush()
}
function createBase (store, key) {
return new Autobase(store.session(), key, {
open,
apply: peerGitApply,
valueEncoding: 'json',
ackInterval: 0,
ackThreshold: 0
})
}
async function swarmReplicate (storeA, storeB, topic, testnet) {
const { bootstrap } = testnet
const swarmA = new Hyperswarm({ bootstrap })
const swarmB = new Hyperswarm({ bootstrap })
swarmA.on('connection', (conn) => storeA.replicate(conn))
swarmB.on('connection', (conn) => storeB.replicate(conn))
await swarmA.join(topic, { server: true, client: false }).flushed()
swarmB.join(topic, { client: true, server: false })
await swarmB.flush()
return { swarmA, swarmB }
}
async function waitForKey (bases, key, maxWait = 10000) {
const start = Date.now()
while (Date.now() - start < maxWait) {
for (const b of bases) await b.update()
let allHave = true
for (const b of bases) {
if (!(await b.view.get(key))) { allHave = false; break }
}
if (allHave) return true
await new Promise(r => setTimeout(r, 300))
}
return false
}
// ─────────────────────────────────────────────────────────────
// Test 1: Reboot persistence — close, reopen, verify multi-writer state
// ─────────────────────────────────────────────────────────────
test('reboot: multi-writer state persists after close+reopen', async (t) => {
t.timeout(30000)
const testnet = await createTestnet(3)
const dirA = tmpPath('a')
const dirB = tmpPath('b')
const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true })
const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true })
// Setup multi-writer
const a = createBase(storeA, null)
await a.ready()
const bootstrapKey = a.local.key
const b = createBase(storeB, bootstrapKey)
await b.ready()
const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet)
await a.append({ type: 'create-issue', id: 1, title: 'from A before writer add', author: 'serverA' })
await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true })
await waitForKey([a, b], 'issue:1')
// Confirm writer set
for (let i = 0; i < 3; i++) {
if (a.writable) await a.append(null)
if (b.writable) await b.append(null)
for (const base of [a, b]) await base.update()
await new Promise(r => setTimeout(r, 300))
}
t.ok(b.writable, 'B writable before reboot')
// B writes an issue
await b.append({ type: 'create-issue', id: 2, title: 'from B before reboot', author: 'serverB' })
await waitForKey([a, b], 'issue:2')
// Close everything
await swarmA.destroy()
await swarmB.destroy()
await a.close()
await b.close()
await storeA.close()
await storeB.close()
// Reopen — simulates server reboot
const storeA2 = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true })
const storeB2 = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true })
const a2 = createBase(storeA2, bootstrapKey)
await a2.ready()
const b2 = createBase(storeB2, bootstrapKey)
await b2.ready()
t.comment('A2 writable: ' + a2.writable + ', members: ' + a2.system.members)
t.comment('B2 writable: ' + b2.writable + ', members: ' + b2.system.members)
// Reconnect
const testnet2 = await createTestnet(3)
const { swarmA: sw2A, swarmB: sw2B } = await swarmReplicate(storeA2, storeB2, a2.discoveryKey, testnet2)
await waitForKey([a2, b2], 'issue:2')
// Verify all data survived reboot
const i1a = await a2.view.get('issue:1')
const i2a = await a2.view.get('issue:2')
const i1b = await b2.view.get('issue:1')
const i2b = await b2.view.get('issue:2')
t.ok(i1a, 'A2 has issue:1')
t.ok(i2a, 'A2 has issue:2')
t.ok(i1b, 'B2 has issue:1')
t.ok(i2b, 'B2 has issue:2')
t.is(i1a.value.author, 'serverA')
t.is(i2a.value.author, 'serverB')
// B is still writable after reboot
t.ok(b2.writable, 'B2 still writable after reboot')
// B can still write new issues after reboot
await b2.append({ type: 'create-issue', id: 3, title: 'from B after reboot', author: 'serverB' })
await waitForKey([a2, b2], 'issue:3')
const i3a = await a2.view.get('issue:3')
t.ok(i3a, 'A2 sees B2 issue written after reboot')
t.is(i3a.value.author, 'serverB')
await sw2A.destroy()
await sw2B.destroy()
await a2.close()
await b2.close()
await storeA2.close()
await storeB2.close()
await testnet.destroy()
await testnet2.destroy()
await cleanup(dirA, dirB)
})
// ─────────────────────────────────────────────────────────────
// Test 2: Concurrent writes — both append before syncing
// ─────────────────────────────────────────────────────────────
test('concurrent: both writers append simultaneously, linearization converges', async (t) => {
t.timeout(30000)
const testnet = await createTestnet(3)
const dirA = tmpPath('a')
const dirB = tmpPath('b')
const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true })
const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true })
const a = createBase(storeA, null)
await a.ready()
const b = createBase(storeB, a.local.key)
await b.ready()
// Add B as writer via pipe (fast, reliable setup)
const s1 = a.replicate(true)
const s2 = b.replicate(false)
s1.pipe(s2).pipe(s1)
await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true })
for (let i = 0; i < 5; i++) {
for (const base of [a, b]) await base.update()
if (a.writable) await a.append(null)
if (b.writable) await b.append(null)
await new Promise(r => setTimeout(r, 200))
}
t.ok(b.writable, 'B is writable')
s1.destroy()
s2.destroy()
await new Promise(r => setTimeout(r, 500))
// Both write issues CONCURRENTLY while disconnected
for (let i = 1; i <= 5; i++) {
await a.append({ type: 'create-issue', id: i, title: 'A-issue-' + i, author: 'A' })
}
for (let i = 6; i <= 10; i++) {
await b.append({ type: 'create-issue', id: i, title: 'B-issue-' + i, author: 'B' })
}
// Reconnect via Hyperswarm
const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet)
// Wait for convergence
await waitForKey([a, b], 'issue:10')
// Verify all 10 issues present on both sides
for (let i = 1; i <= 10; i++) {
const ia = await a.view.get('issue:' + i)
const ib = await b.view.get('issue:' + i)
t.ok(ia, 'A has issue:' + i)
t.ok(ib, 'B has issue:' + i)
}
t.comment('A members: ' + a.system.members)
t.comment('B members: ' + b.system.members)
await swarmA.destroy()
await swarmB.destroy()
await a.close()
await b.close()
await storeA.close()
await storeB.close()
await testnet.destroy()
await cleanup(dirA, dirB)
})
// ─────────────────────────────────────────────────────────────
// Test 3: Disconnect + reconnect — offline writes converge
// ─────────────────────────────────────────────────────────────
test('disconnect: offline writes on both sides converge on reconnect', async (t) => {
t.timeout(30000)
const testnet = await createTestnet(3)
const dirA = tmpPath('a')
const dirB = tmpPath('b')
const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true })
const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true })
const a = createBase(storeA, null)
await a.ready()
const b = createBase(storeB, a.local.key)
await b.ready()
// Setup multi-writer and initial sync
const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet)
await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true })
await a.append({ type: 'create-issue', id: 1, title: 'initial sync test', author: 'A' })
await waitForKey([a, b], 'issue:1')
// Confirm
for (let i = 0; i < 3; i++) {
if (a.writable) await a.append(null)
if (b.writable) await b.append(null)
for (const base of [a, b]) await base.update()
await new Promise(r => setTimeout(r, 300))
}
t.ok(b.writable, 'B writable')
// Disconnect
await swarmA.destroy()
await swarmB.destroy()
await new Promise(r => setTimeout(r, 500))
// Both write while disconnected
await a.append({ type: 'create-issue', id: 2, title: 'A wrote offline', author: 'A' })
await a.append({ type: 'create-issue', id: 3, title: 'A also wrote this offline', author: 'A' })
await b.append({ type: 'create-issue', id: 4, title: 'B wrote offline', author: 'B' })
// Reconnect
const testnet2 = await createTestnet(3)
const { swarmA: sw2A, swarmB: sw2B } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet2)
await waitForKey([a, b], 'issue:4')
// All issues present on both sides
for (let i = 1; i <= 4; i++) {
const ia = await a.view.get('issue:' + i)
const ib = await b.view.get('issue:' + i)
t.ok(ia, 'A has issue:' + i)
t.ok(ib, 'B has issue:' + i)
}
await sw2A.destroy()
await sw2B.destroy()
await a.close()
await b.close()
await storeA.close()
await storeB.close()
await testnet.destroy()
await testnet2.destroy()
await cleanup(dirA, dirB)
})
// ─────────────────────────────────────────────────────────────
// Test 4: Probe validation blocks bogus key in real apply
// ─────────────────────────────────────────────────────────────
test('probe validation: bogus key skipped in pear-git apply, real key works', async (t) => {
t.timeout(20000)
const testnet = await createTestnet(3)
const dirA = tmpPath('a')
const dirB = tmpPath('b')
const storeA = new Corestore(dirA, { primaryKey: Buffer.alloc(32, 0), unsafe: true })
const storeB = new Corestore(dirB, { primaryKey: Buffer.alloc(32, 1), unsafe: true })
const a = createBase(storeA, null)
await a.ready()
const b = createBase(storeB, a.local.key)
await b.ready()
const { swarmA, swarmB } = await swarmReplicate(storeA, storeB, a.discoveryKey, testnet)
// First: add bogus key (simulates the old bug)
const bogusKey = Buffer.alloc(32, 0xde).toString('hex')
await a.append({ type: 'add-writer', key: bogusKey, indexer: true })
// Then: add correct key
await a.append({ type: 'add-writer', key: b.local.key.toString('hex'), indexer: true })
// Issue before and after
await a.append({ type: 'create-issue', id: 1, title: 'survives bogus writer', author: 'A' })
// Sync
await waitForKey([a, b], 'issue:1')
// Confirm
for (let i = 0; i < 3; i++) {
if (a.writable) await a.append(null)
if (b.writable) await b.append(null)
for (const base of [a, b]) await base.update()
await new Promise(r => setTimeout(r, 300))
}
t.ok(b.writable, 'B writable (correct key worked despite bogus key in log)')
t.comment('A members: ' + a.system.members)
// B can write
await b.append({ type: 'create-issue', id: 2, title: 'B writes after bogus+correct', author: 'B' })
await waitForKey([a, b], 'issue:2')
const i2 = await a.view.get('issue:2')
t.ok(i2, 'A sees B issue written after bogus key was skipped')
await swarmA.destroy()
await swarmB.destroy()
await a.close()
await b.close()
await storeA.close()
await storeB.close()
await testnet.destroy()
await cleanup(dirA, dirB)
})