| … | … | @@ -0,0 +1,73 @@ |
| 1 | // Proof: the REAL RepoBase (atproto-room, dependency-free) drives an OverlayRepoStore through the |
| 2 | // substrate adapter identically to a StateBase KV — and the records it writes derive the same repo |
| 3 | // as the Hyperbee RepoStore. This is the record seam that moves the multi-DID path off the autobase. |
| 4 | |
| 5 | import test from 'brittle' |
| 6 | import { mkdtempSync, rmSync } from 'node:fs' |
| 7 | import { join } from 'node:path' |
| 8 | import { tmpdir } from 'node:os' |
| 9 | import b4a from 'b4a' |
| 10 | import { OverlayRepoStore } from '../lib/overlay-repo-store.js' |
| 11 | import { OverlayRepoSubstrate } from '../lib/overlay-repo-substrate.js' |
| 12 | import { RepoStore } from '../lib/repo-store.js' |
| 13 | import { getRepo } from '../lib/read-view.js' |
| 14 | import { RepoBase } from '../../atproto-room/lib/repo-base.js' // dependency-free (pure KV convention) |
| 15 | |
| 16 | const DID = 'did:plc:test' |
| 17 | const KEY = (() => { const k = b4a.alloc(32); k[31] = 7; return k })() |
| 18 | const REV = '3ktestrev00000' |
| 19 | const META = { did: DID, rev: REV, prev: null, privateKey: KEY } |
| 20 | const tmp = (t) => { const d = mkdtempSync(join(tmpdir(), 'ors-sub-')); t.teardown(() => rmSync(d, { recursive: true, force: true })); return d } |
| 21 | |
| 22 | test('RepoBase over the adapter: put/get/list/snapshot/delete work over files', async (t) => { |
| 23 | const store = await new OverlayRepoStore(tmp(t)).ready() |
| 24 | const repo = new RepoBase(new OverlayRepoSubstrate(store), { did: DID }) |
| 25 | |
| 26 | const post = (text, at) => ({ $type: 'app.bsky.feed.post', text, createdAt: at }) |
| 27 | await repo.putRecord('app.bsky.feed.post', '3kaaa', post('hello', '2026-01-01T00:00:00.000Z')) |
| 28 | await repo.putRecord('app.bsky.feed.post', '3kbbb', post('world', '2026-01-02T00:00:00.000Z')) |
| 29 | await repo.putRecord('app.bsky.actor.profile', 'self', { $type: 'app.bsky.actor.profile', displayName: 'T' }) |
| 30 | |
| 31 | t.alike(await repo.getRecord('app.bsky.feed.post', '3kaaa'), post('hello', '2026-01-01T00:00:00.000Z'), 'getRecord round-trips through files') |
| 32 | t.is(await repo.getRecord('app.bsky.feed.post', 'nope'), null, 'missing record → null') |
| 33 | |
| 34 | const posts = await repo.listRecords('app.bsky.feed.post') |
| 35 | t.alike(posts.map((r) => r.rkey).sort(), ['3kaaa', '3kbbb'], 'listRecords scoped to a collection') |
| 36 | t.is((await repo.listRecords()).length, 3, 'listRecords all') |
| 37 | |
| 38 | const snap = await repo.snapshot() |
| 39 | t.is(Object.keys(snap).length, 2, 'two collections in snapshot') |
| 40 | t.ok(snap['app.bsky.feed.post']['3kaaa'], 'snapshot nests collection/rkey/record') |
| 41 | |
| 42 | await repo.deleteRecord('app.bsky.feed.post', '3kbbb') |
| 43 | t.is((await repo.listRecords('app.bsky.feed.post')).length, 1, 'delete removed the record file') |
| 44 | |
| 45 | await store.close() |
| 46 | }) |
| 47 | |
| 48 | test('records written via RepoBase→adapter derive the same repo as a Hyperbee RepoStore', async (t) => { |
| 49 | const RECS = [ |
| 50 | ['app.bsky.actor.profile', 'self', { $type: 'app.bsky.actor.profile', displayName: 'T' }], |
| 51 | ['app.bsky.feed.post', '3kaaa', { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-01-01T00:00:00.000Z' }], |
| 52 | ['app.bsky.feed.post', '3kbbb', { $type: 'app.bsky.feed.post', text: 'world', createdAt: '2026-01-02T00:00:00.000Z' }] |
| 53 | ] |
| 54 | const ov = await new OverlayRepoStore(tmp(t)).ready() |
| 55 | const rb = new RepoBase(new OverlayRepoSubstrate(ov), { did: DID }) |
| 56 | for (const [c, r, rec] of RECS) await rb.putRecord(c, r, rec) |
| 57 | |
| 58 | const hb = await new RepoStore(tmp(t)).ready() |
| 59 | for (const [c, r, rec] of RECS) await hb.put(c, r, rec) |
| 60 | |
| 61 | const a = await getRepo(hb, META); const b = await getRepo(ov, META) |
| 62 | t.is(b.commitCid.toString(), a.commitCid.toString(), 'commit CID identical') |
| 63 | t.ok(b4a.equals(b.carBytes, a.carBytes), 'CAR byte-identical') |
| 64 | |
| 65 | await ov.close(); await hb.close() |
| 66 | }) |
| 67 | |
| 68 | test('substrate is records-only — a reserved / no-separator key is rejected', async (t) => { |
| 69 | const store = await new OverlayRepoStore(tmp(t)).ready() |
| 70 | const sub = new OverlayRepoSubstrate(store) |
| 71 | await t.exception(() => sub.set('__lease__', { x: 1 }), /collection\/rkey/, 'reserved control key rejected') |
| 72 | await store.close() |
| 73 | }) |