Incognito-Wiki/tests/content-audit.test.mjs

422 lines
18 KiB
JavaScript

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'));
const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8'));
await run({ root, docsRoot, manifest, supplemental: structuredClone(supplemental) });
} finally {
await rm(root, { recursive: true, force: true });
}
}
async function expectAuditFailure(mutate, pattern) {
await withAuditFixture(async ({ root, docsRoot, manifest, supplemental }) => {
await mutate({ root, docsRoot, manifest, supplemental });
await assert.rejects(
auditContent({ docsRoot, manifest, supplemental }),
pattern,
);
});
}
test('the full migration audit accounts for every source and destination', async () => {
const manifest = await loadManifest('docs/migration-manifest.json');
const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8'));
const result = await auditContent({
docsRoot: 'src/content/docs',
manifest,
supplemental,
});
assert.equal(result.sourceCount, 29);
assert.equal(result.destinationCount, 28);
assert.equal(result.pageCount, 86);
assert.equal(result.supplementalCount, 58);
});
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 distinguish the original export from the recovered PDF archive', 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 selected export/i);
assert.match(content, /server-side recovery preserved them/i);
assert.match(content, /\[Previous exams and documents\]\(\/previous-exams-and-documents\/\)/);
assert.doesNotMatch(content, /Course summaries and old exams can be found in their respective folders\./);
}
});
test('Bachelor year pages and the current migration report omit obsolete missing-asset notices', async () => {
for (const destination of [
'src/content/docs/bachelor/year-1/index.md',
'src/content/docs/bachelor/year-2/index.md',
]) {
const content = await readFile(destination, 'utf8');
assert.doesNotMatch(content, /Missing source asset|study:dke-schedule\.png|^## Schedule$/m);
}
const report = await readFile('docs/migration-report.md', 'utf8');
assert.doesNotMatch(report, /study:dke-schedule\.png|missing schedule embed|missing-asset notice/i);
});
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 accepts a published page declared in the supplemental registry', async () => {
await withAuditFixture(async ({ docsRoot, manifest, supplemental }) => {
const result = await auditContent({
docsRoot,
manifest,
supplemental,
});
assert.equal(result.pageCount, 86);
assert.equal(result.supplementalCount, 58);
});
});
test('audit rejects a supplemental registry destination missing from published content', async () => {
await expectAuditFailure(async ({ docsRoot }) => {
await rm(join(docsRoot, 'previous-exams-and-documents.md'));
}, /supplemental destination is absent from published content/);
});
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:/);
if (file !== 'src/content/docs/useful-information/laptop-buying-advice.md') {
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',
]) {
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('/', '\\/')}\\)`));
});
}