OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / test / precutover-statebase.test.js

125 lines · javascript ref:
// A PRE-CUTOVER DID must survive moving onto the unified (multi-DID) boot path.
//
// The single-DID path keeps a DID's StateBase in its OWN corestore at <storage>/statebase; the multi-DID
// path opens it as a NAMESPACE of the daemon's shared store. StateBaseV3 derives the local writer from the
// corestore's primaryKey, so those two layouts give the same DID DIFFERENT writers — and because the
// creator's genesis writer is roomSeed-derived it re-opens empty-and-writable, whereupon the boot-write
// block authors a fresh genesis. The DID's history is left behind in the old directory.
//
// wj62 survived that relocation only because a co-owner could backfill it. A SINGLE-HOMED DID (ashatron's
// e2jev: 58 posts, one device, no peer) has nothing to backfill from, which is the entire reason it could
// not move onto the unified path — not its storage backend.
//
// `primary` already means "this DID predates the cutover on this box, keep its paths stable" (flat
// control.sock / swarm-keypair / membrane). It now also keeps the StateBase in place, so nothing is moved,
// copied or deleted — the fix is to STOP relocating.
//
// Test 1 is the CONTROL: it proves the relocation really does lose the history, so test 2 is measured
// against a demonstrated failure rather than an assumption.
//
// Run: bare test/precutover-statebase.test.js
import '../lib/bare-globals.js'
import test from 'brittle'
import fs from 'bare-fs'
import { join } from 'bare-path'
import createTestnet from '@hyperswarm/testnet'
import { bootSwarmDevice, bootMultiDevice, provisionDevice } from '../lib/device-node.js'
import { deviceKeetKeyPair } from 'atproto-room/lib/membership.js'

const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'
const DEV = 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote'
const tmp = () => { const d = '/tmp/hpp-precut-' + Math.floor(Math.random() * 1e9); fs.mkdirSync(d, { recursive: true }); return d }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const waitFor = async (fn, ms = 45000) => { const end = Date.now() + ms; while (Date.now() < end) { if (await fn()) return true; await sleep(400) } return false }
const post = (rk, text) => ({ action: 'create', collection: 'app.bsky.feed.post', rkey: rk, record: { $type: 'app.bsky.feed.post', text, createdAt: '2026-07-27T00:00:00.000Z' } })
const keysOf = (st) => st.records.map((r) => r.collection + '/' + r.rkey).sort()

async function entry () {
  const dk = await deviceKeetKeyPair(DEV)
  const b = await provisionDevice({ rootMnemonic: ROOT, devicePublicKey: dk.publicKey, handle: 'precut.local', pdsUrl: 'https://pds.local' })
  return { deviceMnemonic: DEV, did: b.did, signingKey: b.signingKey, deviceProof: b.deviceProof, rootIdentityKey: b.rootIdentityKey, handle: 'precut.local' }
}

// Bring a DID up the LEGACY way — single-DID path, its StateBase in its own corestore at
// <storage>/statebase — author real records, then shut down. Returns what must survive the move.
async function makeLegacyNode (storage, e) {
  const node = await bootSwarmDevice({ storage, ...e, swarm: false, control: false, signIntervalMs: 0 })
  await node.device.applyWrite(post('3aaa', 'pre-cutover one'))
  await node.device.applyWrite(post('3bbb', 'pre-cutover two'))
  await node.device.sign({ force: true })
  const st = await node.device.state()
  const out = { keys: keysOf(st), head: st.head ? st.head.rev : null, self: st.self, count: st.recordCount }
  await node.close()
  return out
}

test('CONTROL: re-homing a single-homed DID onto the shared store LOSES its history', { timeout: 300000 }, async (t) => {
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }
  const e = await entry()
  const storage = tmp()

  const before = await makeLegacyNode(storage, e)
  t.ok(before.keys.includes('app.bsky.feed.post/3aaa'), 'legacy boot authored its records')
  t.ok(before.head, 'and signed a head')
  t.ok(fs.existsSync(join(storage, 'statebase')), 'its StateBase lives in its own directory')

  // The unified path WITHOUT primary: the StateBase relocates into the shared store.
  const d = await bootMultiDevice({ storage, dids: [{ ...e }], swarmOpts, signIntervalMs: 0 })
  t.teardown(async () => { try { await d.close() } catch {} ; await testnet.destroy() })
  const after = await d.byDid.get(e.did).device.state()

  t.not(keysOf(after).join(','), before.keys.join(','), 'THE HAZARD: the record set did not survive the move')
  t.absent(keysOf(after).includes('app.bsky.feed.post/3aaa'), 'its posts are not in the relocated base')
  t.ok(fs.existsSync(join(storage, 'statebase')), 'the old directory is still on disk — nothing was deleted, just abandoned')
})

test('FIX: a primary (pre-cutover) DID keeps its StateBase and survives the move intact', { timeout: 300000 }, async (t) => {
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }
  const e = await entry()
  const storage = tmp()

  const before = await makeLegacyNode(storage, e)
  t.ok(before.keys.includes('app.bsky.feed.post/3aaa'), 'legacy boot authored its records')

  // Same storage, unified path, primary: true — the pre-cutover marker.
  const d = await bootMultiDevice({ storage, dids: [{ ...e, primary: true }], swarmOpts, signIntervalMs: 0 })
  t.teardown(async () => { try { await d.close() } catch {} ; await testnet.destroy() })
  const node = d.byDid.get(e.did)

  const ok = await waitFor(async () => keysOf(await node.device.state()).includes('app.bsky.feed.post/3aaa'))
  t.ok(ok, 'its records are present on the unified path')

  const after = await node.device.state()
  t.alike(keysOf(after), before.keys, 'the record set is EXACTLY what it was — no history left behind')
  t.is(after.head ? after.head.rev : null, before.head, 'and the signed head is unchanged — no fresh genesis was authored')
  t.is(after.self, before.self, 'same device identity')
  t.is(after.writable, true, 'and it is still writable')

  // The kept-in-place store sits outside the daemon's shared store.replicate, so it must be exposed for
  // explicit replication or the DID would sync with nobody while looking healthy locally.
  t.ok(node._statebaseStore, 'the kept-in-place corestore is exposed for explicit replication')

  // And it can still write: the whole point is a working DID, not just preserved bytes.
  await node.device.applyWrite(post('3ccc', 'after the move'))
  const wrote = await waitFor(async () => keysOf(await node.device.state()).includes('app.bsky.feed.post/3ccc'))
  t.ok(wrote, 'a new write lands after the move — the DID is live on the unified path')
})

test('a DID with no legacy directory is unaffected (already-migrated nodes keep the shared store)', { timeout: 300000 }, async (t) => {
  const testnet = await createTestnet(3)
  const swarmOpts = { bootstrap: testnet.bootstrap }
  const e = await entry()
  const storage = tmp() // fresh: no <storage>/statebase exists

  const d = await bootMultiDevice({ storage, dids: [{ ...e, primary: true }], swarmOpts, signIntervalMs: 0 })
  t.teardown(async () => { try { await d.close() } catch {} ; await testnet.destroy() })
  const node = d.byDid.get(e.did)

  t.absent(node._statebaseStore, 'primary alone does not force a legacy layout when there is no legacy directory')
  await node.device.applyWrite(post('3ddd', 'fresh node'))
  const ok = await waitFor(async () => keysOf(await node.device.state()).includes('app.bsky.feed.post/3ddd'))
  t.ok(ok, 'and the DID works normally on the shared store')
})