import assert from 'node:assert/strict'; import { after, before, test } from 'node:test'; import { cp, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; import { execFileSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import fg from 'fast-glob'; import matter from 'gray-matter'; import { load } from 'cheerio'; import { createLibraryCatalog, extractMarkdownHeadings } from '../src/utils/library-catalog.ts'; import { boundedSearchOffset, normalizeLibrarySearchText, parseLegacyLibrarySearchState, searchLibrary } from '../src/utils/library-search.ts'; import { createPublicationManifest, renderSitemap, robotsFor } from '../src/utils/publication-policy.ts'; import { createLibrarySearchPublication } from '../src/utils/search-publication.ts'; const project = fileURLToPath(new URL('../', import.meta.url)); const site = JSON.parse(await readFile(path.join(project, 'src/data/site.json'), 'utf8')).url; async function contentEntries(root = project) { return Promise.all((await fg('src/content/{pages,blog}/**/*.{md,mdx}', { cwd: root })).sort().map(async (file) => { const source = await readFile(path.join(root, file), 'utf8'); const parsed = matter(source); const [, , collection, ...parts] = file.split('/'); return { id: parts.join('/').replace(/\.mdx?$/, ''), collection, body: parsed.content, data: parsed.data }; })); } async function authorEntries(root = project) { return Promise.all((await fg('src/content/authors/**/*.{md,mdx}', { cwd: root })).sort().map(async (file) => { const parsed = matter(await readFile(path.join(root, file), 'utf8')); return { id: file.replace(/^src\/content\/authors\//, '').replace(/\.mdx?$/, ''), data: parsed.data }; })); } const entries = await contentEntries(); const manifest = createPublicationManifest(entries, site); const catalog = createLibraryCatalog(manifest, await authorEntries()); function record(overrides = {}) { return { url: '/library/example', locale: 'en', title: 'Autism Support Guide', description: 'Practical family resources', headings: ['Planning Daily Routines'], category: 'Library', tags: [], author: { name: 'Rula Diab', slug: 'rula-diab' }, publishedAt: '2026-01-01', ...overrides }; } test('actual catalog groups every eligible post by locale and preserves syndicated noindex records', () => { assert.deepEqual(Object.fromEntries(Object.entries(catalog.byLocale).map(([locale, items]) => [locale, items.length])), { en: 46, es: 12, ar: 7 }); assert.equal(catalog.all.length, 65); for (const slug of ['community-highlight-meet-rula-diab', 'new-aia-scottsdale-office']) { assert.ok(catalog.byLocale.en.some(({ search }) => search.url === `/library/${slug}`)); } assert.equal(new Set(catalog.all.map(({ route }) => route)).size, 65); assert.ok(catalog.all.every(({ search }) => search.author.name === 'Rula Diab')); }); test('catalog ordering is date descending with route ascending as the stable tie-breaker', () => { for (let index = 1; index < catalog.all.length; index++) { const previous = catalog.all[index - 1].search; const current = catalog.all[index].search; const dateOrder = Date.parse(previous.publishedAt) - Date.parse(current.publishedAt); assert.ok(dateOrder > 0 || (dateOrder === 0 && previous.url.localeCompare(current.url, 'en') < 0)); } }); test('catalog fixtures include new eligible posts automatically and exclude drafts', () => { const author = [{ id: 'en/author', data: { lang: 'en', slug: 'author', name: 'Example Author' } }]; const make = (slug, draft = false) => ({ id: `en/${slug}`, collection: 'blog', body: '## Searchable heading', data: { lang: 'en', slug, title: slug, description: '', date: '2026-01-01', author: 'author', category: 'Library', tags: [], draft } }); const fixtureManifest = createPublicationManifest([make('new-post'), make('draft-post', true)], site); const fixture = createLibraryCatalog(fixtureManifest, author); assert.deepEqual(fixture.all.map(({ route }) => route), ['/library/new-post']); assert.deepEqual(fixture.all[0].search.headings, ['Searchable heading']); }); test('catalog rejects duplicate routes and missing or ambiguous locale authors', () => { const source = { id: 'en/example', collection: 'blog', body: '', data: { lang: 'en', slug: 'example', title: 'Example', description: '', date: '2026-01-01', author: 'author' } }; const publication = { route: '/library/example', eligible: true, entry: source }; assert.throws(() => createLibraryCatalog([publication, publication], [{ id: 'a', data: { lang: 'en', slug: 'author', name: 'Author' } }]), /Duplicate Library route/); assert.throws(() => createLibraryCatalog([publication], []), /Missing or ambiguous article author/); assert.throws(() => createLibraryCatalog([publication], [ { id: 'a', data: { lang: 'en', slug: 'author', name: 'Author' } }, { id: 'b', data: { lang: 'en', slug: 'author', name: 'Other' } } ]), /Missing or ambiguous article author/); }); test('heading extraction ignores fenced code and strips Markdown/MDX presentation syntax', () => { assert.deepEqual(extractMarkdownHeadings(`## Visible **heading**\n\n\`\`\`md\n## Hidden code\n\`\`\`\n## [Linked heading](/path)\n## \n## ![Image heading](/image.jpg)`), [ 'Visible heading', 'Linked heading', 'Image heading' ]); }); test('normalization handles case, whitespace, punctuation, Spanish accents, and Arabic marks', () => { assert.equal(normalizeLibrarySearchText(' EVALUACIÓN—ABA ', 'es'), 'evaluacion aba'); assert.equal(normalizeLibrarySearchText('التَّـوحُّد', 'ar'), 'التوحد'); assert.doesNotThrow(() => normalizeLibrarySearchText('\ud800 🙂 ???', 'en')); }); test('search handles empty, exact-title, headings, descriptions, authors, categories and multiple terms', () => { const records = [ record(), record({ url: '/library/second', title: 'Second article', description: 'Autism support', publishedAt: '2026-02-01' }) ]; assert.deepEqual(searchLibrary(records, ''), []); assert.deepEqual(searchLibrary(records, ' '), []); assert.equal(searchLibrary(records, 'Autism Support Guide')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'daily routines')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'family resources')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'Rula Diab')[0].record.url, '/library/example'); assert.equal(searchLibrary([records[0]], 'Library')[0].record.url, '/library/example'); assert.equal(searchLibrary(records, 'autism resources').length, 1); assert.equal(searchLibrary(records, 'nonexistent').length, 0); }); test('search matches Spanish accents and Arabic diacritics without changing display text', () => { const spanish = record({ locale: 'es', title: 'Evaluación del autismo', url: '/es/library/evaluacion' }); const arabic = record({ locale: 'ar', title: 'دليل التَّـوحُّد', url: '/ar/library/guide' }); assert.equal(searchLibrary([spanish], 'evaluacion')[0].record.title, 'Evaluación del autismo'); assert.equal(searchLibrary([arabic], 'التوحد')[0].record.title, 'دليل التَّـوحُّد'); }); test('equal search scores use publication date then normalized route', () => { const records = [ record({ url: '/library/z', title: 'Shared title', publishedAt: '2025-01-01' }), record({ url: '/library/b', title: 'Shared title', publishedAt: '2026-01-01' }), record({ url: '/library/a', title: 'Shared title', publishedAt: '2026-01-01' }) ]; assert.deepEqual(searchLibrary(records, 'shared').map(({ record: item }) => item.url), ['/library/a', '/library/b', '/library/z']); }); test('legacy query parsing gives term precedence, preserves repeated types, and normalizes offset/language', () => { assert.deepEqual(parseLegacyLibrarySearchState('?term=autism&q=ignored&type=BLOG_POST&type=SITE_PAGE&lang=es&offset=20'), { query: 'autism', language: 'es', offset: 20, types: ['BLOG_POST', 'SITE_PAGE'] }); assert.deepEqual(parseLegacyLibrarySearchState('?q=%3Cscript%3E&lang=unknown&offset=-10'), { query: '