Incognito-Wiki/tests/previous-wiki.test.mjs

169 lines
7.5 KiB
JavaScript

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 = `
<div id="index__tree">
<a class="idx_dir" href="/wiki/start?idx=study%3Abachelor">Bachelor</a>
<a class="idx_dir" href="/wiki/start?idx=study%3Abachelor">Bachelor duplicate</a>
<a class="wikilink1" data-wiki-id="study:bachelor:year_1:block_1:calculus">Calculus</a>
<a class="wikilink1" data-wiki-id="study:bachelor:year_1:block_1:calculus">Calculus duplicate</a>
</div>`;
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/data-science-and-ai/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', '<div id="index__tree"><a class="idx_dir" href="?idx=study">Study</a><a data-wiki-id="start">Start</a></div>'],
['/wiki/start?do=index&idx=study', '<div id="index__tree"><a class="idx_dir" href="?idx=study%3Abachelor">Bachelor</a><a data-wiki-id="study">Study</a></div>'],
['/wiki/start?do=index&idx=study%3Abachelor', '<div id="index__tree"><a data-wiki-id="study:bachelor:year_1:block_1:calculus">Calculus</a></div>'],
['/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/);
});