// @pds/core/loader tests - Repository loader from CAR files import { describe, it, expect, beforeEach } from 'vitest'; import { loadRepositoryFromCar, getCarRepoInfo, validateCarFile, } from '../packages/core/src/loader.js'; import { buildCarFile, CID, cborEncodeDagCbor, createCid, cidToString, cidToBytes, createTid, } from '../packages/core/src/repo.js'; import { buildMst } from '../packages/core/src/mst.js'; // In-memory storage for tests function createMockStorage() { /** @type {Map} */ const blocks = new Map(); /** @type {Map} */ const records = new Map(); /** @type {Array<{seq: number, cid: string, rev: string, prev: string|null}>} */ const commits = []; /** @type {{did: string|null, handle: string|null, privateKey: Uint8Array|null}} */ const metadata = { did: null, handle: null, privateKey: null }; return { // Block operations async getBlock(cid) { return blocks.get(cid) || null; }, async putBlock(cid, data) { blocks.set(cid, data); }, // Record operations async getRecord(uri) { const rec = records.get(uri); return rec || null; }, async putRecord(uri, cid, collection, rkey, value) { records.set(uri, { cid, value }); }, async listRecords(collection, cursor, limit) { const all = []; for (const [uri, rec] of records) { if (uri.includes(`/${collection}/`)) { const parts = uri.split('/'); all.push({ uri, cid: rec.cid, value: rec.value, rkey: parts[parts.length - 1] }); } } return { records: all.slice(0, limit), cursor: null }; }, async listAllRecords() { const all = []; for (const [uri, rec] of records) { const parts = uri.replace('at://', '').split('/'); const key = `${parts[1]}/${parts[2]}`; all.push({ key, cid: rec.cid }); } return all; }, async deleteRecord(uri) { records.delete(uri); }, // Commit operations async getLatestCommit() { return commits.length > 0 ? commits[commits.length - 1] : null; }, async putCommit(seq, cid, rev, prev) { commits.push({ seq, cid, rev, prev }); }, // Metadata operations async getDid() { return metadata.did; }, async setDid(did) { metadata.did = did; }, async getHandle() { return metadata.handle; }, async setHandle(handle) { metadata.handle = handle; }, async getPrivateKey() { return metadata.privateKey; }, async setPrivateKey(key) { metadata.privateKey = key; }, async getPreferences() { return []; }, async setPreferences() {}, // Events async getEvents() { return { events: [], cursor: 0 }; }, async putEvent() {}, // Blobs async getBlob() { return null; }, async putBlob() {}, async deleteBlob() {}, async listBlobs() { return { cids: [], cursor: null }; }, async getOrphanedBlobs() { return []; }, async linkBlobToRecord() {}, async unlinkBlobsFromRecord() {}, // Test helpers _getBlockCount() { return blocks.size; }, _getRecordCount() { return records.size; }, _getCommits() { return commits; }, }; } /** * Build a test CAR file with records * @param {string} did * @param {Array<{collection: string, rkey: string, record: object}>} recordsToInclude * @returns {Promise} */ async function buildTestCar(did, recordsToInclude) { /** @type {Array<{cid: string, data: Uint8Array}>} */ const blocks = []; /** @type {Map} */ const blockMap = new Map(); // Encode records and build MST entries /** @type {Array<{key: string, cid: string}>} */ const mstEntries = []; for (const { collection, rkey, record } of recordsToInclude) { const recordBytes = cborEncodeDagCbor(record); const recordCid = cidToString(await createCid(recordBytes)); blocks.push({ cid: recordCid, data: recordBytes }); blockMap.set(recordCid, recordBytes); mstEntries.push({ key: `${collection}/${rkey}`, cid: recordCid }); } // Sort entries for MST mstEntries.sort((a, b) => a.key.localeCompare(b.key)); // Build MST const mstRoot = await buildMst(mstEntries, async (cid, data) => { blocks.push({ cid, data }); blockMap.set(cid, data); }); // Build commit const rev = createTid(); const commit = { did, version: 3, rev, prev: null, data: mstRoot ? new CID(cidToBytes(mstRoot)) : null, }; // Add signature field (empty for test) const signedCommit = { ...commit, sig: new Uint8Array(64) }; const commitBytes = cborEncodeDagCbor(signedCommit); const commitCid = cidToString(await createCid(commitBytes)); blocks.push({ cid: commitCid, data: commitBytes }); return buildCarFile(commitCid, blocks); } describe('loadRepositoryFromCar', () => { let storage; beforeEach(() => { storage = createMockStorage(); }); it('should load empty repository', async () => { const did = 'did:plc:test123'; const car = await buildTestCar(did, []); const result = await loadRepositoryFromCar(car, storage); expect(result.did).toBe(did); expect(result.recordCount).toBe(0); expect(result.blockCount).toBeGreaterThan(0); expect(await storage.getDid()).toBe(did); }); it('should load repository with single record', async () => { const did = 'did:plc:test123'; const car = await buildTestCar(did, [ { collection: 'app.bsky.feed.post', rkey: '3abc123', record: { $type: 'app.bsky.feed.post', text: 'Hello World', createdAt: '2024-01-01T00:00:00Z' }, }, ]); const result = await loadRepositoryFromCar(car, storage); expect(result.did).toBe(did); expect(result.recordCount).toBe(1); const record = await storage.getRecord(`at://${did}/app.bsky.feed.post/3abc123`); expect(record).not.toBeNull(); expect(record?.cid).toBeDefined(); }); it('should load repository with multiple records', async () => { const did = 'did:plc:test456'; const car = await buildTestCar(did, [ { collection: 'app.bsky.feed.post', rkey: '3post1', record: { $type: 'app.bsky.feed.post', text: 'Post 1', createdAt: '2024-01-01T00:00:00Z' }, }, { collection: 'app.bsky.feed.post', rkey: '3post2', record: { $type: 'app.bsky.feed.post', text: 'Post 2', createdAt: '2024-01-02T00:00:00Z' }, }, { collection: 'app.bsky.actor.profile', rkey: 'self', record: { $type: 'app.bsky.actor.profile', displayName: 'Test User' }, }, ]); const result = await loadRepositoryFromCar(car, storage); expect(result.did).toBe(did); expect(result.recordCount).toBe(3); // Verify all records exist expect(await storage.getRecord(`at://${did}/app.bsky.feed.post/3post1`)).not.toBeNull(); expect(await storage.getRecord(`at://${did}/app.bsky.feed.post/3post2`)).not.toBeNull(); expect(await storage.getRecord(`at://${did}/app.bsky.actor.profile/self`)).not.toBeNull(); }); it('should create commit in storage', async () => { const did = 'did:plc:test789'; const car = await buildTestCar(did, [ { collection: 'app.bsky.feed.post', rkey: '3test', record: { $type: 'app.bsky.feed.post', text: 'Test', createdAt: '2024-01-01T00:00:00Z' }, }, ]); const result = await loadRepositoryFromCar(car, storage); const commit = await storage.getLatestCommit(); expect(commit).not.toBeNull(); expect(commit?.cid).toBe(result.commitCid); expect(commit?.rev).toBe(result.rev); expect(commit?.seq).toBe(1); }); }); describe('getCarRepoInfo', () => { it('should extract DID and commit info', async () => { const did = 'did:plc:infotest'; const car = await buildTestCar(did, [ { collection: 'app.bsky.feed.post', rkey: '3test', record: { $type: 'app.bsky.feed.post', text: 'Test', createdAt: '2024-01-01T00:00:00Z' }, }, ]); const info = getCarRepoInfo(car); expect(info.did).toBe(did); expect(info.commitCid).toBeDefined(); expect(info.rev).toBeDefined(); }); }); describe('validateCarFile', () => { it('should validate correct CAR file', async () => { const did = 'did:plc:validtest'; const car = await buildTestCar(did, []); const result = validateCarFile(car); expect(result.valid).toBe(true); expect(result.did).toBe(did); expect(result.error).toBeUndefined(); }); it('should reject malformed CAR', () => { const garbage = new Uint8Array([0xff, 0xff, 0xff]); const result = validateCarFile(garbage); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); });