OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-base / test / migration.test.mjs

47 lines · javascript ref:
// PHASE 0 #5 — MIGRATION READER. importRepo() imports an existing atproto repo (a CAR — the live DID's
// getRepo, or a backup) into a fresh atproto-base and ADOPTS its head, so the atproto chain CONTINUES
// unbroken: the next commit prev-links to the adopted head and its rev strictly increases past it (the
// relay's forward-only cursor rejects a regression — federation-wall). CIDs are preserved (canonical DAG-CBOR).

import test from 'brittle'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { deriveAtprotoKey, createAtprotoIdentity } from 'atproto-identity'
import { readCar } from 'atproto-repo/lib/car.js'
import { verifyRepo } from 'atproto-repo'
import { AtprotoBase } from '../lib/base.js'
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)
const post = (text) => ({ $type: 'app.bsky.feed.post', text, createdAt: '2026-07-23T00:00:00.000Z', langs: ['en'] })
const R = (k, v) => ({ key: 'app.bsky.feed.post/' + k, value: post(v) })
function mkBase () { const dir = mkdtempSync(join(tmpdir(), 'mig-')); const base = new AtprotoBase({ path: dir, did: ID.did, privateKey: KEY.privateKey, ackInterval: 100 }); return { base, dir, close: async () => { await base.close(); rmSync(dir, { recursive: true, force: true }) } } }
const recCount = async (car) => (await verifyRepo(await readCar(car))).records

test('migration: importRepo adopts an existing repo CAR and continues the chain unbroken', async (t) => {
  // A "legacy" repo, as if fetched from the live DID's getRepo: 2 records at legacyRev.
  const legacyRev = nextRev(null)
  const legacy = await projectCommit({ records: [R('3a', 'one'), R('3b', 'two')], did: ID.did, rev: legacyRev, prev: null, privateKey: KEY.privateKey })

  const A = mkBase(); await A.base.ready()
  t.teardown(() => A.close())

  const adopted = await A.base.importRepo(legacy.carBytes)
  t.is(adopted.records, 2, 'imported 2 records')
  t.is(adopted.commitCid.toString(), legacy.commitCid.toString(), 'adopted the legacy head commit (CID preserved)')

  const served = await A.base.getRepo()
  t.is(await recCount(served.carBytes), 2, 'getRepo serves the 2 imported records')
  t.is(served.commitCid.toString(), legacy.commitCid.toString(), 'getRepo commit == the adopted head')

  // A continuation post: the chain must extend from the adopted head, not restart.
  await A.base.putRecord('app.bsky.feed.post', '3c', post('after migration'))
  const w = await A.base.emitFirehose()
  t.is(w.signedCommit.prev.toString(), legacy.commitCid.toString(), 'next commit prev-links to the adopted head (unbroken chain)')
  t.ok(w.rev > legacyRev, 'rev strictly increases past the adopted rev (relay forward-only cursor accepts)')
  t.is(await recCount((await A.base.getRepo()).carBytes), 3, '3 records after the continuation post')
})