import '../lib/bare-globals.js'
// RepoBase over an injected StateBaseV3-compatible KV log (mocked in-memory). Proves the repo-op
// convention: collection/rkey keying, put/get/delete round-trip, list + snapshot, reserved-key
// skipping, and input guards. No autobase/Bare needed — the substrate is injected.
import test from 'brittle'
import { RepoBase, recordKey, splitKey } from '../lib/repo-base.js'
// Minimal StateBaseV3-shaped KV log: async set/del/get/list, values are objects (StateBaseV3
// JSON-encodes internally but get/list return decoded objects — mirror that).
function mockStateBase () {
const m = new Map()
return {
m,
async set (k, v) { m.set(k, v) },
async del (k) { m.delete(k) },
async get (k) { return m.has(k) ? m.get(k) : null },
async list (prefix) { const o = {}; for (const [k, v] of m) if (k.startsWith(prefix)) o[k] = v; return o }
}
}
const POST = 'app.bsky.feed.post'
const PROFILE = 'app.bsky.actor.profile'
test('key helpers: collection/rkey ↔ key', (t) => {
t.is(recordKey(POST, '3abc'), POST + '/3abc')
t.alike(splitKey(POST + '/3abc'), { collection: POST, rkey: '3abc' })
t.is(splitKey('__members__'), null, 'reserved (no separator) → null')
// NSID keeps its dots; only the FIRST slash separates
t.alike(splitKey('app.bsky.feed.post/3a'), { collection: 'app.bsky.feed.post', rkey: '3a' })
})
test('put → get → delete round-trip', async (t) => {
const rb = new RepoBase(mockStateBase(), { did: 'did:plc:test' })
const rec = { $type: POST, text: 'hello', createdAt: '2026-07-04T00:00:00.000Z' }
t.alike(await rb.putRecord(POST, '3abc', rec), { collection: POST, rkey: '3abc' })
t.alike(await rb.getRecord(POST, '3abc'), rec, 'record round-trips')
t.is(await rb.getRecord(POST, 'nope'), null, 'missing → null')
await rb.deleteRecord(POST, '3abc')
t.is(await rb.getRecord(POST, '3abc'), null, 'deleted → null')
})
test('listRecords + snapshot; scope by collection; skip reserved keys', async (t) => {
const sb = mockStateBase()
// a reserved StateBase key that is NOT a repo record — must be skipped by list/snapshot
await sb.set('__members__', { some: 'roster' })
const rb = new RepoBase(sb)
await rb.putRecord(POST, '3a', { $type: POST, text: 'a' })
await rb.putRecord(POST, '3b', { $type: POST, text: 'b' })
await rb.putRecord(PROFILE, 'self', { $type: PROFILE, displayName: 'x' })
const all = await rb.listRecords()
t.is(all.length, 3, 'lists 3 records, __members__ skipped')
t.absent(all.some(r => r.collection === '__members__'), 'reserved key not surfaced')
const posts = await rb.listRecords(POST)
t.is(posts.length, 2, 'scoped to the post collection')
t.ok(posts.every(r => r.collection === POST), 'only posts')
const snap = await rb.snapshot()
t.is(Object.keys(snap[POST]).length, 2, 'snapshot groups posts by rkey')
t.is(snap[PROFILE].self.displayName, 'x', 'snapshot nests collection/rkey/record')
t.absent(snap.__members__, 'snapshot excludes reserved keys')
})
test('input guards', async (t) => {
const rb = new RepoBase(mockStateBase())
await t.exception(rb.putRecord('a/b', 'r', { $type: 'x' }), /must not contain/, 'collection with "/" rejected')
await t.exception(rb.putRecord(POST, 'r/x', { $type: 'x' }), /must not contain/, 'rkey with "/" rejected')
await t.exception(rb.putRecord(POST, '3a', 'not-an-object'), /must be an object/, 'non-object record rejected')
t.exception(() => new RepoBase(null), /statebase/, 'missing statebase rejected')
})