import assert from 'node:assert/strict'; import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { auditContent } from '../scripts/audit-content.mjs'; import { loadManifest } from '../scripts/lib/migration.mjs'; async function withAuditFixture(run) { const root = await mkdtemp(join(tmpdir(), 'incognito-audit-')); const docsRoot = join(root, 'src/content/docs'); try { await mkdir(join(root, 'src/content'), { recursive: true }); await cp('src/content/docs', docsRoot, { recursive: true }); await mkdir(join(root, 'docs'), { recursive: true }); await cp('docs/migration-report.md', join(root, 'docs/migration-report.md')); await mkdir(join(root, 'public'), { recursive: true }); const manifest = structuredClone(await loadManifest('docs/migration-manifest.json')); await run({ root, docsRoot, manifest }); } finally { await rm(root, { recursive: true, force: true }); } } async function expectAuditFailure(mutate, pattern) { await withAuditFixture(async ({ root, docsRoot, manifest }) => { await mutate({ root, docsRoot, manifest }); await assert.rejects( auditContent({ docsRoot, manifest }), pattern, ); }); } test('the full migration audit accounts for every source and destination', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); const result = await auditContent({ docsRoot: 'src/content/docs', manifest, }); assert.equal(result.sourceCount, 29); assert.equal(result.destinationCount, 28); assert.equal(result.pageCount, 28); }); test('published source pages leave the sole H1 to Starlight frontmatter rendering', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))]; for (const destination of destinations) { const content = await readFile(destination, 'utf8'); assert.doesNotMatch(content, /^#\s+/m, `${destination} contains a body H1`); } }); test('present-day association introductions use the current DACS name', async () => { for (const destination of [ 'src/content/docs/index.mdx', 'src/content/docs/about-incognito.md', ]) { const content = await readFile(destination, 'utf8'); assert.match(content, /Department of Advanced Computing Sciences/); assert.doesNotMatch(content, /study association for the Department of Data Science & Artificial Intelligence/); } }); test('programme overviews disclose that promised summaries and exams were absent from the export', async () => { for (const destination of [ 'src/content/docs/bachelor/index.md', 'src/content/docs/master-ai/index.md', 'src/content/docs/master-dsdm/index.md', ]) { const content = await readFile(destination, 'utf8'); assert.match(content, /not present in the export/i); assert.doesNotMatch(content, /Course summaries and old exams can be found in their respective folders\./); } }); test('the home historical warning uses the standard Starlight caution', async () => { const content = await readFile('src/content/docs/index.mdx', 'utf8'); assert.match(content, /:::caution\[Historical information\]\nThis information originated in the previous wiki and may be outdated\.[\s\S]*?\n:::/); }); test('Useful Information scopes contribution guidance to repository maintainers', async () => { const content = await readFile('src/content/docs/useful-information/index.md', 'utf8'); assert.match(content, /Repository maintainers/); assert.doesNotMatch(content, /please create a new page/i); }); test('DSDM year pages rely on their page-level caution without redundant inner notes', async () => { for (const destination of [ 'src/content/docs/master-dsdm/year-1/index.md', 'src/content/docs/master-dsdm/year-2/index.md', ]) { const content = await readFile(destination, 'utf8'); assert.doesNotMatch(content, /:::note\[Historical (?:course list|study structure)\]/); } }); test('all 29 migration-report rows classify link handling truthfully', async () => { const report = await readFile('docs/migration-report.md', 'utf8'); const expectedPrefixes = new Map([ ['pages/start.txt', '**Converted source links:**'], ['pages/study.txt', '**Converted source links:**'], ['pages/study/msv_incognito.txt', '**Converted source link:**'], ['pages/study/bachelor.txt', '**Converted source link:**'], ['pages/study/bachelor/year_1.txt', '**Newly verified links:**'], ['pages/study/bachelor/year_1/project_1-1.txt', '**None.**'], ['pages/study/bachelor/year_1/project_1-2.txt', '**None.**'], ['pages/study/bachelor/year_2.txt', '**Newly verified links:**'], ['pages/study/bachelor/year_2/honours_programme.txt', '**None.**'], ['pages/study/bachelor/year_2/project_2-1.txt', '**Converted plain-text reference:**'], ['pages/study/bachelor/year_2/project_2-2.txt', '**Converted plain-text reference:**'], ['pages/study/bachelor/year_3.txt', '**Newly verified links:**'], ['pages/study/bachelor/year_3/bachelors_thesis.txt', '**None.**'], ['pages/study/bachelor/year_3/honours_programme.txt', '**None.**'], ['pages/study/bachelor/year_3/project_3-1.txt', '**Converted plain-text reference:**'], ['pages/study/bachelor/year_3/study_abroad.txt', '**Newly verified link:**'], ['pages/study/master_ai.txt', '**Converted source link:**'], ['pages/study/master_ai/year_1.txt', '**Newly verified links:**'], ['pages/study/master_ai/year_1/project_mai_1.txt', '**None.**'], ['pages/study/master_ai/year_1/project_mai_2.txt', '**None.**'], ['pages/study/master_ai/year_2.txt', '**None.**'], ['pages/study/master_dsdm.txt', '**Converted source link:**'], ['pages/study/master_dsdm/year_1.txt', '**None.**'], ['pages/study/master_dsdm/year_2.txt', '**None.**'], ['pages/study/useful_information.txt', '**Converted source link:**'], ['pages/study/useful_information/pages/dke_locations.txt', '**Converted source links:**'], ['pages/study/useful_information/pages/handy_locations.txt', '**None.**'], ['pages/study/useful_information/pages/it_services.txt', '**Converted source links:**'], ['pages/study/useful_information/pages/laptop_buy_advice.txt', '**None.**'], ]); const rows = report.split('\n').filter((line) => line.startsWith('| `pages/')); assert.equal(rows.length, expectedPrefixes.size); for (const row of rows) { const cells = row.split(' | '); const source = cells[0].match(/^\| `([^`]+)`$/)?.[1]; assert.ok(source, `could not parse report source from ${row}`); assert.ok(expectedPrefixes.has(source), `unexpected report source ${source}`); assert.ok( cells[4].startsWith(expectedPrefixes.get(source)), `${source} link handling must start with ${expectedPrefixes.get(source)}`, ); } assert.match(report, /\| Link handling \|/); assert.doesNotMatch(report, /Surfgroepen|DKE file server|old namespace link/i); assert.match( report, /pages\/study\/bachelor\/year_3\/study_abroad\.txt[^\n]*\*\*Newly verified link:\*\* the official UM study-abroad page/, ); }); test('audit rejects a published page with missing frontmatter', async () => { await expectAuditFailure(async ({ docsRoot }) => { await writeFile(join(docsRoot, 'about-incognito.md'), 'Missing frontmatter.\n'); }, /missing YAML frontmatter/); }); test('audit rejects frontmatter with an empty required field', async () => { await expectAuditFailure(async ({ docsRoot }) => { await writeFile(join(docsRoot, 'about-incognito.md'), [ '---', 'title:', 'description: Still described.', '---', '', 'Body.', ].join('\n')); }, /missing non-empty title frontmatter/); }); test('audit rejects syntactically malformed YAML frontmatter', async () => { await expectAuditFailure(async ({ docsRoot }) => { await writeFile(join(docsRoot, 'about-incognito.md'), [ '---', 'title: "unterminated', 'description: Still described.', '---', '', 'Body.', ].join('\n')); }, /malformed YAML frontmatter/); }); test('audit rejects forbidden DokuWiki syntax in published content', async () => { await expectAuditFailure(async ({ docsRoot }) => { const page = join(docsRoot, 'about-incognito.md'); await writeFile(page, `${await readFile(page, 'utf8')}\n[[forbidden:link]]\n`); }, /contains forbidden DokuWiki token/); }); test('audit rejects a missing published document', async () => { await expectAuditFailure(async ({ docsRoot }) => { await rm(join(docsRoot, 'about-incognito.md')); }, /missing manifest destination: src\/content\/docs\/about-incognito\.md/); }); test('audit rejects an extra Markdown document outside the manifest destination set', async () => { await expectAuditFailure(async ({ docsRoot }) => { await writeFile(join(docsRoot, 'extra.md'), [ '---', 'title: Extra', 'description: Not in the migration contract.', '---', ].join('\n')); }, /published content file is absent from manifest: src\/content\/docs\/extra\.md/); }); test('audit rejects a directory where the manifest requires a regular file', async () => { await expectAuditFailure(async ({ docsRoot }) => { const destination = join(docsRoot, 'about-incognito.md'); await rm(destination); await mkdir(destination); }, /manifest destination is not a regular file: src\/content\/docs\/about-incognito\.md/); }); test('audit rejects coordinated manifest and report drift from the canonical sources', async () => { await expectAuditFailure(async ({ root, manifest }) => { const original = 'pages/study/msv_incognito.txt'; const fabricated = 'pages/study/fabricated.txt'; manifest.entries[2].source = fabricated; const reportPath = join(root, 'docs/migration-report.md'); const report = await readFile(reportPath, 'utf8'); await writeFile(reportPath, report.replace(original, fabricated)); }, /canonical migration contract/); }); test('audit rejects coordinated destination, report, and published-file drift', async () => { await expectAuditFailure(async ({ root, docsRoot, manifest }) => { const original = 'src/content/docs/about-incognito.md'; const drifted = 'src/content/docs/association.md'; manifest.entries[2].destination = drifted; await rename( join(docsRoot, 'about-incognito.md'), join(docsRoot, 'association.md'), ); const reportPath = join(root, 'docs/migration-report.md'); const report = await readFile(reportPath, 'utf8'); await writeFile(reportPath, report.replace(original, drifted)); }, /canonical migration contract/); }); test('audit rejects a report row whose mapping differs from the manifest', async () => { await expectAuditFailure(async ({ root }) => { const reportPath = join(root, 'docs/migration-report.md'); const report = await readFile(reportPath, 'utf8'); await writeFile( reportPath, report.replace( '`src/content/docs/about-incognito.md` | page', '`src/content/docs/not-about-incognito.md` | page', ), ); }, /migration report mapping differs from manifest: pages\/study\/msv_incognito\.txt/); }); test('audit rejects docs roots that merely end with a false src/content/docs suffix', async () => { const root = await mkdtemp(join(tmpdir(), 'incognito-false-root-')); try { const docsRoot = join(root, 'not-src/content/docs'); await mkdir(docsRoot, { recursive: true }); const manifest = await loadManifest('docs/migration-manifest.json'); await assert.rejects( auditContent({ docsRoot, manifest }), /docsRoot must identify src\/content\/docs/, ); } finally { await rm(root, { recursive: true, force: true }); } }); test('audit rejects to-be-studied directories on either publication path', async () => { await expectAuditFailure(async ({ root, docsRoot }) => { await mkdir(join(docsRoot, 'to-be-studied'), { recursive: true }); await mkdir(join(root, 'public/to-be-studied'), { recursive: true }); }, /to-be-studied must not appear under src\/content\/docs/); }); const forbidden = [/\[\[/, /\{\{/, /~~NOCACHE~~/, /NEWPAGE>/, /indexmenu>/]; for (const file of [ 'src/content/docs/bachelor/index.md', 'src/content/docs/bachelor/year-1/index.md', 'src/content/docs/bachelor/year-1/project-1-1.md', 'src/content/docs/bachelor/year-1/project-1-2.md', 'src/content/docs/bachelor/year-2/index.md', 'src/content/docs/bachelor/year-2/honours-programme.md', 'src/content/docs/bachelor/year-2/project-2-1.md', 'src/content/docs/bachelor/year-2/project-2-2.md', 'src/content/docs/bachelor/year-3/index.md', 'src/content/docs/bachelor/year-3/bachelors-thesis.md', 'src/content/docs/bachelor/year-3/honours-programme.md', 'src/content/docs/bachelor/year-3/project-3-1.md', 'src/content/docs/bachelor/year-3/study-abroad.md', 'src/content/docs/master-ai/index.md', 'src/content/docs/master-ai/year-1/index.md', 'src/content/docs/master-ai/year-1/research-project-1.md', 'src/content/docs/master-ai/year-1/research-project-2.md', 'src/content/docs/master-ai/year-2/index.md', 'src/content/docs/master-dsdm/index.md', 'src/content/docs/master-dsdm/year-1/index.md', 'src/content/docs/master-dsdm/year-2/index.md', 'src/content/docs/useful-information/index.md', 'src/content/docs/useful-information/dke-locations.md', 'src/content/docs/useful-information/handy-locations.md', 'src/content/docs/useful-information/it-services.md', 'src/content/docs/useful-information/laptop-buying-advice.md', ]) { test(`${file} contains clean migrated Markdown`, async () => { const content = await readFile(file, 'utf8'); assert.match(content, /^---\n[\s\S]*title:/); assert.match(content, /This information originated in the previous wiki and may be outdated\./); for (const pattern of forbidden) assert.doesNotMatch(content, pattern); }); } for (const file of [ 'src/content/docs/useful-information/it-services.md', 'src/content/docs/useful-information/laptop-buying-advice.md', ]) { test(`${file} prominently identifies historical guidance`, async () => { const content = await readFile(file, 'utf8'); assert.match(content, /:::caution\[Historical information\]/); assert.match(content, /This information originated in the previous wiki and may be outdated\./); }); } test('Handy Locations retains its awaiting-content notices', async () => { const content = await readFile( 'src/content/docs/useful-information/handy-locations.md', 'utf8', ); assert.match(content, /:::note\[Awaiting content\]/); assert.match(content, /This section is awaiting content\./); }); test('Year 2 honours programme retains its awaiting-content notice', async () => { const content = await readFile( 'src/content/docs/bachelor/year-2/honours-programme.md', 'utf8', ); assert.match(content, /:::note\[Awaiting content\]/); assert.match(content, /This page is awaiting content\./); }); test('Year 3 honours programme retains its awaiting-content notice', async () => { const content = await readFile( 'src/content/docs/bachelor/year-3/honours-programme.md', 'utf8', ); assert.match(content, /:::note\[Awaiting content\]/); assert.match(content, /This page is awaiting content\./); }); test('Year 3 study abroad page identifies 2022 advice as historical', async () => { const content = await readFile( 'src/content/docs/bachelor/year-3/study-abroad.md', 'utf8', ); assert.match(content, /2022/); assert.match(content, /must be verified/i); }); for (const { file, correctHref, shallowHref } of [ { file: 'src/content/docs/bachelor/year-2/project-2-1.md', correctHref: '../../year-1/project-1-1/', shallowHref: '../year-1/project-1-1/', }, { file: 'src/content/docs/bachelor/year-2/project-2-2.md', correctHref: '../../year-1/project-1-2/', shallowHref: '../year-1/project-1-2/', }, ]) { test(`${file} links to its Year 1 prerequisite from the nested route`, async () => { const content = await readFile(file, 'utf8'); assert.match(content, new RegExp(`\\]\\(${correctHref.replaceAll('/', '\\/')}\\)`)); assert.doesNotMatch(content, new RegExp(`\\]\\(${shallowHref.replaceAll('/', '\\/')}\\)`)); }); }