OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store / test / repo-store.test.js

87 lines · javascript ref:
// Slice 1 (Holepunch-first): the classic-hyperbee RepoStore, verified against the
// MST oracle. The point: records stored + read back through real Hyperbee produce
// the SAME MST root as building directly from the records — so the native store is a
// faithful, byte-transparent home, and the bridge (atproto-repo) can serialize it.

import test from 'brittle'
import { readFileSync, mkdtempSync, rmSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { tmpdir } from 'node:os'
import * as dagCbor from '@ipld/dag-cbor'
import { RepoStore } from '../lib/repo-store.js'
// bridge oracle (cross-module; pure-JS path):
import { buildMst } from 'atproto-repo/lib/mst.js'
import { cidForBytesSync } from 'atproto-repo/lib/cid.js'
import { readCar, loadCommit, listRecords } from 'atproto-repo/lib/repo.js'

const here = dirname(fileURLToPath(import.meta.url))
const goldenCar = readFileSync(join(here, 'vectors', 'bridgetest-repo.car'))

// records [{key, bytes}] -> MST root CID (this is what the bridge will do)
const mstRootOf = (recs) => buildMst(recs.map(r => ({ key: r.key, value: cidForBytesSync(r.bytes) }))).root
const splitKey = (k) => { const i = k.indexOf('/'); return [k.slice(0, i), k.slice(i + 1)] }

const newStore = async (t) => {
  const dir = mkdtempSync(join(tmpdir(), 'repostore-'))
  const s = await new RepoStore(dir).ready()
  t.teardown(async () => { try { await s.close() } catch {} rmSync(dir, { recursive: true, force: true }) })
  return s
}

// 1. round-trip
test('put -> get round-trips a record', async (t) => {
  const s = await newStore(t)
  const rec = { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-07-01T00:00:00.000Z' }
  await s.put('app.bsky.feed.post', '3aaa', rec)
  const got = await s.get('app.bsky.feed.post', '3aaa')
  // compare canonical DAG-CBOR bytes (JSON.stringify is key-order-sensitive; dag-cbor canonicalises)
  t.ok(Buffer.from(dagCbor.encode(got)).equals(Buffer.from(dagCbor.encode(rec))),
    'put -> get round-trips a record')
})

// 2. delete
test('del removes a record', async (t) => {
  const s = await newStore(t)
  await s.put('app.bsky.feed.post', '3aaa', { $type: 'x', text: 'a' })
  await s.del('app.bsky.feed.post', '3aaa')
  const got = await s.get('app.bsky.feed.post', '3aaa')
  t.is(got, null, 'del removes a record')
})

// 3. list by collection + records() ordering
test('list scopes to a collection; records() is key-ordered', async (t) => {
  const s = await newStore(t)
  await s.put('app.bsky.feed.post', '3bbb', { text: 'b' })
  await s.put('app.bsky.feed.post', '3aaa', { text: 'a' })
  await s.put('app.bsky.actor.profile', 'self', { displayName: 'me' })
  const posts = []
  for await (const r of s.list('app.bsky.feed.post')) posts.push(r.rkey)
  const allKeys = (await s.records()).map(r => r.key)
  const listOk = JSON.stringify(posts) === JSON.stringify(['3aaa', '3bbb'])
  const orderedOk = JSON.stringify(allKeys) === JSON.stringify([...allKeys].sort())
  const scopedOk = !posts.includes('self')
  t.ok(listOk && orderedOk && scopedOk, 'list scopes to a collection; records() is key-ordered')
})

// 4. ORACLE A — real golden repo: records through the store -> golden MST root
test('golden repo records via store -> MST root == commit.data', async (t) => {
  const car = await readCar(goldenCar)
  const commitData = loadCommit(car).value.data
  const s = await newStore(t)
  for (const r of listRecords(car)) { const [c, rk] = splitKey(r.key); await s.put(c, rk, r.value) }
  const root = mstRootOf(await s.records())
  t.ok(root.equals(commitData), 'golden repo records via store -> MST root == commit.data')
})

// 5. ORACLE B — store is byte-transparent to MST building (multi-record)
test('store->MST root == direct->MST root (byte-transparent)', async (t) => {
  const recs = []
  for (let i = 0; i < 40; i++) recs.push({ key: `app.test.record/${String(i).padStart(4, '0')}`, record: { $type: 'app.test.record', n: i, note: `row ${i}` } })
  const s = await newStore(t)
  for (const r of recs) { const [c, rk] = splitKey(r.key); await s.put(c, rk, r.record) }
  const storeRoot = mstRootOf(await s.records())
  const directRoot = mstRootOf(recs.map(r => ({ key: r.key, bytes: dagCbor.encode(r.record) })))
  t.ok(storeRoot.equals(directRoot), 'store->MST root == direct->MST root (byte-transparent)')
})