import { parseEmbed, scanImagePosts } from '../postScanner'; const DID = 'did:plc:test123'; const imgRef = (cid, _alt = '') => ({ $type: 'blob', ref: { $link: cid }, mimeType: 'image/jpeg', size: 1000, }); describe('parseEmbed', () => { it('parses app.bsky.embed.images with a missing-alt image', () => { const embed = { $type: 'app.bsky.embed.images', images: [{ alt: '', image: imgRef('cid1'), aspectRatio: { width: 2, height: 1 } }], }; const parsed = parseEmbed(embed, DID); expect(parsed.embedType).toBe('app.bsky.embed.images'); expect(parsed.missing).toBe(1); expect(parsed.images[0].cdnUrl).toBe(`https://cdn.bsky.app/img/feed_fullsize/plain/${DID}/cid1@jpeg`); expect(parsed.images[0].hasAlt).toBe(false); }); it('counts images with alt text as not missing', () => { const embed = { $type: 'app.bsky.embed.images', images: [{ alt: 'a photo', image: imgRef('cid2'), aspectRatio: { width: 1, height: 1 } }], }; const parsed = parseEmbed(embed, DID); expect(parsed.missing).toBe(0); expect(parsed.images[0].hasAlt).toBe(true); }); it('parses recordWithMedia (media is an images embed)', () => { const embed = { $type: 'app.bsky.embed.recordWithMedia', record: { uri: 'at://x/app.bsky.feed.post/abc' }, media: { $type: 'app.bsky.embed.images', images: [{ alt: '', image: imgRef('cid3') }, { alt: 'ok', image: imgRef('cid4') }] }, }; const parsed = parseEmbed(embed, DID); expect(parsed.embedType).toBe('app.bsky.embed.recordWithMedia'); expect(parsed.missing).toBe(1); expect(parsed.images.length).toBe(2); }); it('returns null for recordWithMedia whose media is not images', () => { const embed = { $type: 'app.bsky.embed.recordWithMedia', record: { uri: 'at://x/app.bsky.feed.post/abc' }, media: { $type: 'app.bsky.embed.external', external: {} }, }; expect(parseEmbed(embed, DID)).toBeNull(); }); it('parses video embeds with a single alt slot', () => { const noAlt = parseEmbed({ $type: 'app.bsky.embed.video', video: imgRef('cid5') }, DID); expect(noAlt.missing).toBe(1); expect(noAlt.images[0].kind).toBe('video'); const withAlt = parseEmbed({ $type: 'app.bsky.embed.video', video: imgRef('cid5'), alt: 'clips' }, DID); expect(withAlt.missing).toBe(0); }); it('returns null for embeds with no editable alt', () => { expect(parseEmbed(null, DID)).toBeNull(); expect(parseEmbed({ $type: 'app.bsky.embed.external', external: {} }, DID)).toBeNull(); }); }); describe('scanImagePosts', () => { const page = (rows, cursor) => ({ data: { records: rows, cursor } }); it('paginates until no cursor and reports hits per page', async () => { const agent = { api: { com: { atproto: { repo: { listRecords: jest .fn() .mockResolvedValueOnce( page([ { uri: `at://${DID}/app.bsky.feed.post/rkey1`, cid: 'cidA', value: { text: 'hi', embed: { $type: 'app.bsky.embed.images', images: [{ alt: '', image: imgRef('b1') }] } }, }, ], 'cursor-1') ) .mockResolvedValueOnce( page([ { uri: `at://${DID}/app.bsky.feed.post/rkey2`, cid: 'cidB', value: { text: 'no media', embed: { $type: 'app.bsky.embed.external', external: {} } }, }, ], null) ), }, }, }, }, }; const onPage = jest.fn(); const result = await scanImagePosts(agent, DID, { onPage }); expect(agent.api.com.atproto.repo.listRecords).toHaveBeenCalledTimes(2); expect(result.posts.length).toBe(1); expect(result.posts[0].rkey).toBe('rkey1'); expect(result.posts[0].missingAltCount).toBe(1); expect(onPage).toHaveBeenCalledWith({ cursor: 'cursor-1', hits: 1 }); expect(onPage).toHaveBeenCalledWith({ cursor: null, hits: 1 }); }); it('resumes from startCursor', async () => { const agent = { api: { com: { atproto: { repo: { listRecords: jest.fn().mockResolvedValue(page([], null)) } } } }, }; await scanImagePosts(agent, DID, { startCursor: 'resume-here' }); expect(agent.api.com.atproto.repo.listRecords).toHaveBeenCalledWith( expect.objectContaining({ repo: DID, collection: 'app.bsky.feed.post', limit: 100, cursor: 'resume-here' }) ); }); });