import assert from 'node:assert/strict'; import { access, mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { classifyPreviousPage, convertCourseSource, crawlPreviousWiki, destinationForPageId, extractSitemapEntries, } from '../scripts/lib/previous-wiki.mjs'; test('extracts unique namespace and page IDs from a DokuWiki sitemap', () => { const html = `
`; assert.deepEqual(extractSitemapEntries(html), { namespaces: ['study:bachelor'], pageIds: ['study:bachelor:year_1:block_1:calculus'], }); }); test('classifies course pages separately from represented and excluded pages', () => { const represented = new Set(['study:bachelor:year_1']); assert.equal(classifyPreviousPage('study:bachelor:year_1', represented), 'represented'); assert.equal( classifyPreviousPage('study:bachelor:year_1:block_1:calculus', represented), 'migrate', ); assert.equal(classifyPreviousPage('wiki:syntax', represented), 'excluded'); assert.equal(classifyPreviousPage('sidebar', represented), 'excluded'); assert.equal(classifyPreviousPage('study:useful_information', represented), 'excluded'); assert.equal( classifyPreviousPage( 'study:master_ai:year_2:block_1:placeholder', represented, '====== placeholder ======\n\nThis course is a placeholder', ), 'empty', ); }); test('maps in-scope course IDs to stable Starlight destinations', () => { assert.equal( destinationForPageId('study:bachelor:year_1:block_1:discrete_mathematics'), 'src/content/docs/bachelor/year-1/block-1/discrete-mathematics.md', ); assert.equal( destinationForPageId('study:master_ai:year_2:block_3:agents'), 'src/content/docs/master-ai/year-2/block-3/agents.md', ); assert.equal(destinationForPageId('study:useful_information'), null); }); test('recursively crawls sitemap namespaces and saves every raw page once', async () => { const outputRoot = await mkdtemp(join(tmpdir(), 'incognito-previous-wiki-')); const requests = []; const responses = new Map([ ['/wiki/start?do=index', ''], ['/wiki/start?do=index&idx=study', ''], ['/wiki/start?do=index&idx=study%3Abachelor', ''], ['/wiki/_export/raw/start', '====== Home ======'], ['/wiki/_export/raw/study', '====== Study ======'], ['/wiki/_export/raw/study%3Abachelor%3Ayear_1%3Ablock_1%3Acalculus', '====== Calculus ======'], ]); try { const manifest = await crawlPreviousWiki({ capturedAt: '2026-08-03', outputRoot, fetchText: async (path) => { requests.push(path); if (!responses.has(path)) throw new Error(`unexpected request ${path}`); return responses.get(path); }, }); assert.equal(manifest.pages.length, 3); assert.equal(manifest.failures.length, 0); assert.deepEqual(manifest.pages.map(({ id }) => id), [ 'start', 'study', 'study:bachelor:year_1:block_1:calculus', ]); assert.equal( await readFile(join(outputRoot, 'pages/study/bachelor/year_1/block_1/calculus.txt'), 'utf8'), '====== Calculus ======', ); assert.equal(new Set(requests).size, requests.length); } finally { await rm(outputRoot, { recursive: true, force: true }); } }); test('converts a DokuWiki course source and its file list to clean Starlight Markdown', () => { const markdown = convertCourseSource({ pageId: 'study:bachelor:year_1:block_1:calculus', sourceUrl: 'https://msvincognito.nl/wiki/study/bachelor/year_1/block_1/calculus', source: [ '====== Calculus ======', '', '==== Full course description ====', '', 'See [[https://example.com/course|the course guide]].\\\\', '', '==== Files ====', '', '{{filelist>:study:bachelor:year_1:block_1:calculus:*&showdate=1}}', '', '~~NOCACHE~~', ].join('\n'), pdfs: [{ mediaId: 'study:bachelor:year_1:block_1:calculus:exam.pdf', publicUrl: '/media/legacy-dokuwiki/study/bachelor/year_1/block_1/calculus/exam.pdf', }], }); assert.match(markdown, /^---\ntitle: Calculus\ndescription:/); assert.match(markdown, /:::caution\[Historical information\]/); assert.match(markdown, /## Full course description/); assert.match(markdown, /\[the course guide\]\(https:\/\/example\.com\/course\)/); assert.match(markdown, /\[Exam\]\(\/media\/legacy-dokuwiki\/study\/bachelor\/year_1\/block_1\/calculus\/exam\.pdf\)/); assert.match(markdown, /\[Previous wiki page\]\(https:\/\/msvincognito\.nl\/wiki\/study\/bachelor\/year_1\/block_1\/calculus\)/); assert.doesNotMatch(markdown, /\[\[|\{\{|~~NOCACHE~~|^# /m); }); test('replaces a generic Course Title heading with the course page name', () => { const markdown = convertCourseSource({ pageId: 'study:master_dsdm:year_1:block_5:planning_and_scheduling', sourceUrl: 'https://msvincognito.nl/wiki/study/master_dsdm/year_1/block_5/planning_and_scheduling', source: '====== Course Title ======\n\nCourse description.', pdfs: [], }); assert.match(markdown, /^---\ntitle: Planning And Scheduling\n/); }); test('all 54 substantive live course pages are published and registered', async () => { const rows = (await readFile('docs/live-course-recovery.tsv', 'utf8')).trim().split(/\r?\n/).slice(1) .map((line) => { const [id, sourceUrl, status, destination] = line.split('\t'); return { id, sourceUrl, status, destination }; }); const migrated = rows.filter(({ status }) => status === 'migrated'); assert.equal(migrated.length, 54); assert.equal(rows.filter(({ status }) => status === 'migrate').length, 0); const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8')); const supplementalByDestination = new Map(supplemental.map((entry) => [entry.destination, entry])); const recoveredSidebar = await readFile('src/config/recovered-course-sidebar.mjs', 'utf8'); for (const record of migrated) { await access(record.destination); const content = await readFile(record.destination, 'utf8'); assert.match(content, /:::caution\[Historical information\]/, record.destination); assert.match(content, new RegExp(record.sourceUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), record.destination); assert.equal(supplementalByDestination.get(record.destination)?.source, record.sourceUrl); assert.equal(supplementalByDestination.get(record.destination)?.category, 'live-course-recovery'); const slug = record.destination.replace(/^src\/content\/docs\//, '').replace(/\.md$/, ''); assert.match(recoveredSidebar, new RegExp(`slug: '${slug}'`), slug); } const report = await readFile('docs/live-course-recovery.md', 'utf8'); assert.match(report, /Course pages migrated \| 54/); assert.doesNotMatch(report, /Substantive course pages to migrate/); });