169 lines
7.5 KiB
JavaScript
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/);
|
|
});
|