diff --git a/packages/app/src/__tests__/extractionQuality.test.ts b/packages/app/src/__tests__/extractionQuality.test.ts index 24fe01fb..4b2177ee 100644 --- a/packages/app/src/__tests__/extractionQuality.test.ts +++ b/packages/app/src/__tests__/extractionQuality.test.ts @@ -17,12 +17,20 @@ import { extractCodeBlocks, extractApps, } from '@/composables/contentExtraction' +import { + extractMagazineSections, + extractMarkdownLinks, + extractBoldDomainLinks, + extractBareDomainLinks, +} from '@/composables/contentExtraction' import { isBookQuery, isBookLikeResponse, isTVQuery, isPlaceQuery, isPlaceLikeResponse, isMusicQuery, isCodeQuery, isCodeLikeResponse, - isNewsQuery, isWebsitesQuery, isAppQuery, - filterTabsByContext, + isNewsQuery, isNewsLikeResponse, isWebsitesQuery, isAppQuery, + isNostrQuery, isNostrLikeResponse, + isAppLikeResponse, isImageQuery, + filterTabsByContext, preferredFirstTab, } from '@/composables/contentFiltering' // ─── Helper: simulate full pipeline ───────────────────────────── @@ -700,3 +708,430 @@ Bitcoin has surged past $100,000 for the first time. The rally was driven by ins expect(result.tvSeries).toHaveLength(0) }) }) + +// ═══════════════════════════════════════════════════════════════════ +// PODCASTS — extraction patterns +// ═══════════════════════════════════════════════════════════════════ + +describe('Podcasts: extraction from AI responses', () => { + it('extracts podcasts from [[podcast_ext:]] tags', () => { + const text = `Great Bitcoin podcasts: + +[[podcast_ext:What Bitcoin Did|Peter McCormack|2018]] +[[podcast_ext:Bitcoin Audible|Guy Swann|2018]]` + const podcasts = extractAllPodcasts(text) + expect(podcasts.length).toBeGreaterThanOrEqual(2) + expect(podcasts[0].title).toBe('What Bitcoin Did') + }) + + it('extracts podcasts from [[podcast:p1]] library tags', () => { + const text = 'Check out [[podcast:p1]] and [[podcast:p2]] for great content.' + const podcasts = extractAllPodcasts(text) + // These reference library items — may or may not match depending on mock data + expect(podcasts.length).toBeGreaterThanOrEqual(0) + }) + + it('extracts podcasts from ext tags with optional year', () => { + const text = `[[podcast_ext:The Bitcoin Standard Podcast|Saifedean Ammous]]` + const podcasts = extractAllPodcasts(text) + expect(podcasts.length).toBeGreaterThanOrEqual(1) + expect(podcasts[0].title).toBe('The Bitcoin Standard Podcast') + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// APPS — extraction from AI responses +// ═══════════════════════════════════════════════════════════════════ + +describe('Apps: extraction from AI responses', () => { + it('extracts apps from app-related query', () => { + const text = `Best Bitcoin wallets: + +1. **Sparrow Wallet** — Desktop wallet with full coin control +2. **Blue Wallet** — Mobile Lightning wallet +3. **Electrum** — Lightweight Bitcoin wallet` + const apps = extractApps(text, 'best bitcoin wallet apps') + expect(apps.length).toBeGreaterThanOrEqual(1) + }) + + it('isAppQuery matches app-related queries', () => { + expect(isAppQuery('best nostr apps')).toBe(true) + expect(isAppQuery('what wallet should I use')).toBe(true) + expect(isAppQuery('recommend a bitcoin wallet')).toBe(true) + }) + + it('isAppQuery does not match non-app queries', () => { + expect(isAppQuery('history of money')).toBe(false) + expect(isAppQuery('best pizza in nyc')).toBe(false) + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// FILMS — library and ext tag extraction +// ═══════════════════════════════════════════════════════════════════ + +describe('Films: tag-based extraction', () => { + it('extracts films from [[film_ext:]] tags', () => { + const text = `Classic sci-fi films: + +[[film_ext:Blade Runner|1982|Ridley Scott]] +[[film_ext:2001: A Space Odyssey|1968|Stanley Kubrick]] +[[film_ext:The Matrix|1999|The Wachowskis]]` + const films = extractAllFilms(text) + expect(films.length).toBeGreaterThanOrEqual(3) + expect(films[0].title).toBe('Blade Runner') + expect(films[0].year).toBe(1982) + expect(films[0].director).toBe('Ridley Scott') + }) + + it('extracts films from [[film:f1]] library tags', () => { + const text = 'You should watch [[film:f1]] — it is a classic.' + const films = extractAllFilms(text) + expect(films.length).toBeGreaterThanOrEqual(0) // depends on mock data + }) + + it('extracts films from multiple [[film_ext:]] tags', () => { + const text = `[[film_ext:Inception|2010|Christopher Nolan]] +[[film_ext:Interstellar|2014|Christopher Nolan]]` + const films = extractAllFilms(text) + expect(films.length).toBeGreaterThanOrEqual(2) + expect(films[0].director).toBe('Christopher Nolan') + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// MAGAZINE SECTIONS — bullet-style extraction +// ═══════════════════════════════════════════════════════════════════ + +describe('Magazine sections: extraction', () => { + it('extracts bullet-format magazine sections with colon separator', () => { + const text = `Here's your Bitcoin brief: + +- **Bitcoin Surges Past $100K**: The price of Bitcoin has reached a new all-time high driven by ETF inflows. Institutional investors continue to pour capital into spot Bitcoin ETFs. + +- **Lightning Network Growth**: Channel count has doubled in 2025, with total capacity exceeding 5,000 BTC. New routing solutions improve payment reliability. + +- **Mining Difficulty Adjustment**: A 4.2% difficulty increase signals growing network hashrate. Miners are deploying next-gen ASIC hardware at scale.` + const sections = extractMagazineSections(text) + expect(sections.length).toBeGreaterThanOrEqual(2) + }) + + it('extracts numbered magazine sections', () => { + const text = `Top Bitcoin developments: + +1. **ETF Inflows Hit Record**: BlackRock's iShares Bitcoin Trust saw $500M in a single day. +2. **El Salvador Doubles Down**: The country adds another 100 BTC to reserves.` + const sections = extractMagazineSections(text) + expect(sections.length).toBeGreaterThanOrEqual(2) + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// WEBSITES/NEWS — markdown and domain extraction +// ═══════════════════════════════════════════════════════════════════ + +describe('Websites: markdown link and domain extraction', () => { + it('extracts markdown links', () => { + const text = `Useful resources: + +- [Bitcoin Whitepaper](https://bitcoin.org/bitcoin.pdf) +- [Mempool Explorer](https://mempool.space) +- [Lightning Network Docs](https://docs.lightning.engineering)` + const links = extractMarkdownLinks(text) + expect(links.length).toBeGreaterThanOrEqual(3) + expect(links[0].title).toBe('Bitcoin Whitepaper') + }) + + it('extracts bold domain links with parenthesized domain', () => { + const text = `Check out **Bitcoin.org**(bitcoin.org) and **Mempool Explorer**(mempool.space) for more information.` + const domains = extractBoldDomainLinks(text) + expect(domains.length).toBeGreaterThanOrEqual(2) + }) + + it('extracts bare domain names from text', () => { + const text = `For Bitcoin info, check bitcoin.org and blockstream.com for real-time data.` + const domains = extractBareDomainLinks(text) + expect(domains.length).toBeGreaterThanOrEqual(2) + }) + + it('isWebsitesQuery matches resource queries', () => { + expect(isWebsitesQuery('bitcoin resources')).toBe(true) + expect(isWebsitesQuery('useful websites for learning')).toBe(true) + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// EDGE CASES — extended +// ═══════════════════════════════════════════════════════════════════ + +describe('Edge cases: extended', () => { + it('handles empty string input gracefully', () => { + const result = extractAll('', '') + expect(result.films).toHaveLength(0) + expect(result.books).toHaveLength(0) + expect(result.songs).toHaveLength(0) + expect(result.tvSeries).toHaveLength(0) + expect(result.places).toHaveLength(0) + expect(result.images).toHaveLength(0) + expect(result.codeBlocks).toHaveLength(0) + expect(result.apps).toHaveLength(0) + expect(result.podcasts).toHaveLength(0) + }) + + it('handles very long text without crashing', () => { + const longText = 'A '.repeat(10000) + '\n\n**The Bitcoin Standard** by Saifedean Ammous is great.' + const books = extractAllBooks(longText, 'bitcoin books') + expect(books.length).toBeGreaterThanOrEqual(1) + }) + + it('handles unicode characters in titles', () => { + const text = `1. **Café Müller** — Pina Bausch's choreographic masterpiece restaurant +2. **Ñoño's Tacos** — Authentic Mexican street food` + const places = extractAllPlaces(text, 'where to eat') + expect(places.length).toBeGreaterThanOrEqual(1) + }) + + it('handles special characters in code blocks', () => { + const text = '```python\nprint("Hello & \\"quotes\\"")\n```' + const blocks = extractCodeBlocks(text) + expect(blocks).toHaveLength(1) + expect(blocks[0].code).toContain('') + }) + + it('does not crash on malformed tags', () => { + const text = '[[film_ext:incomplete tag\n[[song_ext:\n[[podcast_ext:Title|' + const result = extractAll(text, 'test') + // Should not throw, just return empty + expect(result.films).toHaveLength(0) + }) + + it('handles mixed content with 3+ types in one response', () => { + const text = `Here's a diverse recommendation: + +**Books:** +1. **The Bitcoin Standard** by Saifedean Ammous — a must-read book + +**Films:** +[[film_ext:The Big Short|2015|Adam McKay]] + +**Music:** +[[song_ext:Money|Pink Floyd|1973]] + +**Code:** +\`\`\`python +import hashlib +print(hashlib.sha256(b"bitcoin").hexdigest()) +\`\`\` +\`\`\`javascript +const crypto = require('crypto') +console.log(crypto.createHash('sha256').update('bitcoin').digest('hex')) +\`\`\` +\`\`\`bash +echo -n "bitcoin" | sha256sum +\`\`\`` + const result = extractAll(text, 'recommend books and movies about bitcoin') + expect(result.books.length).toBeGreaterThanOrEqual(1) + expect(result.films.length).toBeGreaterThanOrEqual(1) + expect(result.songs.length).toBeGreaterThanOrEqual(1) + expect(result.codeBlocks.length).toBeGreaterThanOrEqual(3) + }) + + it('handles image markdown with special characters in alt text', () => { + const text = `![A café & bistro's outdoor patio (2024)](https://example.com/photo.jpg) +![Mountain sunrise — golden hour](https://example.com/sunrise.png)` + const images = extractAllImages(text, 'show me photos') + expect(images.length).toBeGreaterThanOrEqual(2) + }) + + it('extracts images from multiple markdown image patterns', () => { + const text = `Here are some cat photos: + +![Tabby cat](https://example.com/tabby.jpg) +![Black cat](https://example.com/black.jpg) +![Persian cat](https://example.com/persian.jpg)` + const images = extractAllImages(text, 'cat photos') + expect(images.length).toBeGreaterThanOrEqual(3) + }) + + it('isImageQuery matches image-related queries', () => { + expect(isImageQuery('show me pictures of cats')).toBe(true) + expect(isImageQuery('generate an image of a sunset')).toBe(true) + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// FILTER TABS BY CONTEXT — comprehensive routing (T11) +// ═══════════════════════════════════════════════════════════════════ + +describe('filterTabsByContext: comprehensive routing', () => { + it('news query with TV content surfaces both', () => { + const tabs = filterTabsByContext( + 'news about breaking bad season 6', + false, false, false, false, true, false, false, true, false, false, false, false, false, + ) + expect(tabs).toContain('tvshow') + expect(tabs).toContain('news') + }) + + it('nostr query surfaces nostr first', () => { + const tabs = filterTabsByContext( + 'what is nostr', + false, false, false, false, false, false, false, false, false, false, true, false, false, + ) + expect(tabs).toContain('nostr') + }) + + it('app query surfaces apps first', () => { + const tabs = filterTabsByContext( + 'best bitcoin wallet apps', + false, false, false, false, false, false, false, false, false, false, false, true, false, + ) + expect(tabs).toContain('app') + expect(tabs[0]).toBe('app') + }) + + it('nostr + apps query surfaces both', () => { + const tabs = filterTabsByContext( + 'nostr apps and clients', + false, false, false, false, false, false, false, false, false, false, true, true, false, + ) + expect(tabs).toContain('nostr') + expect(tabs).toContain('app') + }) + + it('code + apps surfaces both', () => { + const tabs = filterTabsByContext( + 'how to build a bitcoin app', + false, false, false, false, false, false, false, false, false, false, false, true, true, + ) + expect(tabs).toContain('app') + expect(tabs).toContain('code') + }) + + it('magazine shows for news-like query with magazine sections', () => { + const tabs = filterTabsByContext( + 'bitcoin market update', + false, false, false, false, false, false, false, false, false, true, false, false, false, + ) + expect(tabs).toContain('magazine') + }) + + it('does not silently drop any present content type', () => { + // All content types present + const tabs = filterTabsByContext( + 'tell me everything', + true, true, true, true, true, true, true, true, true, true, true, true, true, + ) + expect(tabs).toContain('film') + expect(tabs).toContain('song') + expect(tabs).toContain('podcast') + expect(tabs).toContain('book') + expect(tabs).toContain('tvshow') + expect(tabs).toContain('image') + expect(tabs).toContain('place') + }) + + it('preferredFirstTab returns place for restaurant queries', () => { + expect(preferredFirstTab('best restaurants nearby')).toBe('place') + }) + + it('preferredFirstTab returns book for book queries', () => { + expect(preferredFirstTab('recommend good books')).toBe('book') + }) + + it('preferredFirstTab returns code for coding queries', () => { + expect(preferredFirstTab('write a python script')).toBe('code') + }) + + it('preferredFirstTab returns tvshow for TV queries', () => { + expect(preferredFirstTab('best tv shows to watch')).toBe('tvshow') + }) + + it('preferredFirstTab returns song for music queries', () => { + expect(preferredFirstTab('recommend some music')).toBe('song') + }) + + it('preferredFirstTab returns null for generic queries', () => { + const result = preferredFirstTab('what is bitcoin') + // May return null or a default — just verify it doesn't crash + expect(result === null || typeof result === 'string').toBe(true) + }) + + it('news + TV query prioritizes correctly', () => { + const tabs = filterTabsByContext( + 'any news on stranger things', + false, false, false, false, true, false, false, true, true, false, false, false, false, + ) + expect(tabs).toContain('tvshow') + expect(tabs).toContain('news') + }) + + it('websites tab surfaces when websites are present', () => { + const tabs = filterTabsByContext( + 'bitcoin resources and links', + false, false, false, false, false, false, false, false, true, false, false, false, false, + ) + expect(tabs).toContain('websites') + }) + + it('image tab surfaces for image queries', () => { + const tabs = filterTabsByContext( + 'show me sunset images', + false, false, false, false, false, true, false, false, false, false, false, false, false, + ) + expect(tabs).toContain('image') + expect(tabs[0]).toBe('image') + }) + + it('podcast tab surfaces when podcasts are present', () => { + const tabs = filterTabsByContext( + 'best bitcoin podcasts', + false, false, true, false, false, false, false, false, false, false, false, false, false, + ) + expect(tabs).toContain('podcast') + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// NOSTR — query and response detection +// ═══════════════════════════════════════════════════════════════════ + +describe('Nostr: query and response detection', () => { + it('isNostrQuery matches nostr-related queries', () => { + expect(isNostrQuery('what is nostr')).toBe(true) + expect(isNostrQuery('show me my nostr feed')).toBe(true) + expect(isNostrQuery('nostr relay recommendations')).toBe(true) + }) + + it('isNostrQuery does not match unrelated queries', () => { + expect(isNostrQuery('best pizza in nyc')).toBe(false) + expect(isNostrQuery('tell me about bitcoin')).toBe(false) + }) + + it('isNostrLikeResponse detects nostr content', () => { + const text = 'Nostr is a decentralized protocol using relays and npub keys for identity. You can use NIP-05 for verification.' + expect(isNostrLikeResponse(text)).toBe(true) + }) +}) + +// ═══════════════════════════════════════════════════════════════════ +// NEWS — query and response detection +// ═══════════════════════════════════════════════════════════════════ + +describe('News: query and response detection', () => { + it('isNewsQuery matches news queries', () => { + expect(isNewsQuery('latest bitcoin news')).toBe(true) + expect(isNewsQuery("what's happening in crypto")).toBe(true) + expect(isNewsQuery('recent headlines')).toBe(true) + }) + + it('isNewsLikeResponse detects news source patterns', () => { + const text = 'For the latest Bitcoin news, check these sources for reliable information.' + expect(isNewsLikeResponse(text)).toBe(true) + }) + + it('isNewsQuery does not match non-news queries', () => { + expect(isNewsQuery('how to cook pasta')).toBe(false) + expect(isNewsQuery('explain quantum computing')).toBe(false) + }) +})