310 lines
12 KiB
TypeScript
310 lines
12 KiB
TypeScript
/**
|
|
* Tests for @docmd/plugin-okf
|
|
*
|
|
* Verifies plugin descriptor + end-to-end onPostBuild behavior with a
|
|
* mocked context (3 pages: explicit type, missing type, noindex).
|
|
* Run with: `pnpm --filter @docmd/plugin-okf test`
|
|
*/
|
|
|
|
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
|
|
import { plugin, onPostBuild } from '../src/index.js';
|
|
|
|
describe('plugin descriptor', () => {
|
|
it('has the expected name', () => {
|
|
assert.equal(plugin.name, 'okf');
|
|
});
|
|
|
|
it('declares post-build capability', () => {
|
|
assert.ok(Array.isArray(plugin.capabilities));
|
|
assert.ok(plugin.capabilities.includes('post-build'));
|
|
});
|
|
|
|
it('has a semver version string', () => {
|
|
assert.match(plugin.version, /^\d+\.\d+\.\d+/);
|
|
});
|
|
});
|
|
|
|
describe('onPostBuild', () => {
|
|
let tmpDir: string;
|
|
|
|
before(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-test-'));
|
|
});
|
|
|
|
after(async () => {
|
|
try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { /* cleanup best-effort */ }
|
|
});
|
|
|
|
it('generates the bundle, filters noindex, lints missing types', async () => {
|
|
const bundleDir = path.join(tmpDir, 'site');
|
|
await fs.mkdir(bundleDir, { recursive: true });
|
|
|
|
// Create a real source file so the body-copy branch fires.
|
|
const srcDir = path.join(tmpDir, 'src');
|
|
await fs.mkdir(srcDir, { recursive: true });
|
|
const typedSrc = path.join(srcDir, 'typed.md');
|
|
const untypedSrc = path.join(srcDir, 'untyped.md');
|
|
const hiddenSrc = path.join(srcDir, 'hidden.md');
|
|
await fs.writeFile(typedSrc, '---\ntitle: Typed\ntype: guide\n---\n\n# Typed body\n');
|
|
await fs.writeFile(untypedSrc, '---\ntitle: Untyped\n---\n\n# Untyped body\n');
|
|
await fs.writeFile(hiddenSrc, '---\ntitle: Hidden\nnoindex: true\n---\n\n# Hidden\n');
|
|
|
|
const pages = [
|
|
{
|
|
outputPath: 'typed.html',
|
|
frontmatter: { title: 'Typed', type: 'guide', description: 'Has explicit type.' },
|
|
sourcePath: typedSrc,
|
|
rawMarkdown: ''
|
|
},
|
|
{
|
|
outputPath: 'untyped.html',
|
|
frontmatter: { title: 'Untyped', description: 'No type set.' },
|
|
sourcePath: untypedSrc,
|
|
rawMarkdown: ''
|
|
},
|
|
{
|
|
outputPath: 'hidden.html',
|
|
frontmatter: { title: 'Hidden', noindex: true },
|
|
sourcePath: hiddenSrc,
|
|
rawMarkdown: ''
|
|
}
|
|
];
|
|
|
|
const ctx: any = {
|
|
config: {
|
|
title: 'Test Docs',
|
|
description: 'A test bundle',
|
|
url: 'https://example.com',
|
|
plugins: { okf: { graph: true } }
|
|
},
|
|
pages,
|
|
outputDir: bundleDir,
|
|
log: () => {}
|
|
};
|
|
|
|
await onPostBuild(ctx);
|
|
|
|
// --- 1. okf.yaml exists + concepts === 2 ---
|
|
const okfYamlPath = path.join(bundleDir, 'okf', 'okf.yaml');
|
|
const okfYaml = await fs.readFile(okfYamlPath, 'utf8');
|
|
assert.match(okfYaml, /stats:/);
|
|
assert.match(okfYaml, /concepts: 2/);
|
|
|
|
// --- 2. index.md + graph.html + graph.json + concepts/*.md + _meta/* ---
|
|
const okfRoot = path.join(bundleDir, 'okf');
|
|
const indexMd = await fs.readFile(path.join(okfRoot, 'index.md'), 'utf8');
|
|
assert.match(indexMd, /Knowledge Catalog/);
|
|
assert.match(indexMd, /guide/); // typed page lives under type=guide
|
|
assert.match(indexMd, /Untyped/); // fallback concept listed
|
|
assert.match(indexMd, /Graph viewer/); // graph enabled → link present
|
|
await fs.access(path.join(okfRoot, 'graph/index.html'));
|
|
await fs.access(path.join(okfRoot, 'graph/graph.json'));
|
|
await fs.access(path.join(okfRoot, 'concepts', 'typed.md'));
|
|
await fs.access(path.join(okfRoot, 'concepts', 'untyped.md'));
|
|
await fs.access(path.join(okfRoot, '_meta', 'bundle.json'));
|
|
await fs.access(path.join(okfRoot, '_meta', 'lint-report.txt'));
|
|
|
|
// --- 3. hidden.md was filtered out ---
|
|
await fs.access(path.join(okfRoot, 'concepts', 'hidden.md')).then(
|
|
() => { throw new Error('hidden page should have been filtered'); },
|
|
() => {}
|
|
);
|
|
|
|
// --- 4. Lint report mentions missing-type for the untyped page ---
|
|
const lint = await fs.readFile(path.join(okfRoot, '_meta', 'lint-report.txt'), 'utf8');
|
|
assert.match(lint, /missing-type/);
|
|
assert.match(lint, /\/untyped\//);
|
|
|
|
// --- 5. Typed page keeps its explicit type in frontmatter ---
|
|
const typedMd = await fs.readFile(path.join(okfRoot, 'concepts', 'typed.md'), 'utf8');
|
|
assert.match(typedMd, /^---\n[\s\S]*?type: guide[\s\S]*?---/);
|
|
assert.match(typedMd, /source:\s*"?https:\/\/example\.com\/typed\/"?/);
|
|
|
|
// --- 6. Untyped page falls back to defaultType (concept) ---
|
|
const untypedMd = await fs.readFile(path.join(okfRoot, 'concepts', 'untyped.md'), 'utf8');
|
|
assert.match(untypedMd, /type: concept/);
|
|
|
|
// --- 7. okf.yaml has correct by_type counts ---
|
|
assert.match(okfYaml, /guide: 1/);
|
|
assert.match(okfYaml, /concept: 1/);
|
|
|
|
// --- 8. bundle.json mirrors the manifest ---
|
|
const bundleJson = JSON.parse(await fs.readFile(path.join(okfRoot, '_meta', 'bundle.json'), 'utf8'));
|
|
assert.equal(bundleJson.stats.concepts, 2);
|
|
assert.deepEqual(bundleJson.stats.by_type, { guide: 1, concept: 1 });
|
|
});
|
|
|
|
it('respects config.plugins.okf === false (exits cleanly)', async () => {
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-noop-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'x', plugins: { okf: false } },
|
|
pages: [],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
// Nothing should be created.
|
|
await fs.access(path.join(tmp2, 'okf')).then(
|
|
() => { throw new Error('okf bundle should not exist'); },
|
|
() => {}
|
|
);
|
|
} finally {
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('respects config.plugins.okf.enabled === false (exits cleanly)', async () => {
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-enabled-false-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'x', plugins: { okf: { enabled: false } } },
|
|
pages: [],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
await fs.access(path.join(tmp2, 'okf')).then(
|
|
() => { throw new Error('okf bundle should not exist'); },
|
|
() => {}
|
|
);
|
|
} finally {
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('respects capability filter (capabilities does not include post-build)', async () => {
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-cap-filter-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'x', plugins: { okf: { capabilities: ['head'] } } },
|
|
pages: [],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
await fs.access(path.join(tmp2, 'okf')).then(
|
|
() => { throw new Error('okf bundle should not exist'); },
|
|
() => {}
|
|
);
|
|
} finally {
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('runs by default when no plugins.okf entry is set in the config (core plugin)', async () => {
|
|
// OKF is a core plugin — it's auto-loaded and runs
|
|
// with empty opts when the user hasn't configured it. This test
|
|
// guards the default-enabled contract: a project with no
|
|
// `plugins.okf` entry at all still gets an OKF bundle generated.
|
|
// The graph viewer is opt-in (graph:true) — it must
|
|
// NOT be generated by default, even when OKF itself runs.
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-default-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'Default', url: 'https://example.com' /* no plugins field at all */ },
|
|
pages: [{
|
|
outputPath: 'index.html',
|
|
frontmatter: { title: 'Home', description: 'Landing' },
|
|
sourcePath: '',
|
|
rawMarkdown: '# Home'
|
|
}],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
// Bundle SHOULD exist because the plugin runs with default opts.
|
|
const yaml = await fs.readFile(path.join(tmp2, 'okf', 'okf.yaml'), 'utf8');
|
|
assert.match(yaml, /name: default/);
|
|
assert.match(yaml, /concepts: 1/);
|
|
// Graph viewer is opt-in → no graph artefacts on disk by default.
|
|
const graphCheck = (p: string) => fs.access(p).then(
|
|
() => { throw new Error(`${p} should NOT exist when graph is not enabled`); },
|
|
() => {}
|
|
);
|
|
await graphCheck(path.join(tmp2, 'okf', 'graph/index.html'));
|
|
await graphCheck(path.join(tmp2, 'okf', 'graph/graph.json'));
|
|
await graphCheck(path.join(tmp2, 'okf', 'graph/graph.js'));
|
|
await graphCheck(path.join(tmp2, 'okf', 'graph/graph.css'));
|
|
const indexMd = await fs.readFile(path.join(tmp2, 'okf', 'index.md'), 'utf8');
|
|
assert.doesNotMatch(indexMd, /Graph viewer/);
|
|
} finally {
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('writes the graph viewer only when config.plugins.okf.graph === true', async () => {
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-graph-on-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'Graph', url: 'https://example.com', plugins: { okf: { graph: true } } },
|
|
pages: [{
|
|
outputPath: 'a.html',
|
|
frontmatter: { title: 'A', type: 'guide' },
|
|
sourcePath: '',
|
|
rawMarkdown: '# A'
|
|
}],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
const okfRoot = path.join(tmp2, 'okf');
|
|
await fs.access(path.join(okfRoot, 'graph/index.html'));
|
|
await fs.access(path.join(okfRoot, 'graph/graph.json'));
|
|
await fs.access(path.join(okfRoot, 'graph/graph.js'));
|
|
await fs.access(path.join(okfRoot, 'graph/graph.css'));
|
|
const indexMd = await fs.readFile(path.join(okfRoot, 'index.md'), 'utf8');
|
|
assert.match(indexMd, /Graph viewer/);
|
|
const graphJson = JSON.parse(await fs.readFile(path.join(okfRoot, 'graph/graph.json'), 'utf8'));
|
|
assert.equal(graphJson.nodes.length, 1);
|
|
assert.equal(graphJson.nodes[0].id, 'a');
|
|
} finally {
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('honours the legacy generateGraphViewer flag and warns about deprecation', async () => {
|
|
const tmp2 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-legacy-graph-'));
|
|
const warnings: string[] = [];
|
|
const origLog = console.log;
|
|
console.log = (...args: any[]) => warnings.push(args.join(' '));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'Legacy', url: 'https://example.com', plugins: { okf: { generateGraphViewer: true } } },
|
|
pages: [{
|
|
outputPath: 'a.html',
|
|
frontmatter: { title: 'A', type: 'guide' },
|
|
sourcePath: '',
|
|
rawMarkdown: '# A'
|
|
}],
|
|
outputDir: tmp2,
|
|
log: () => {}
|
|
});
|
|
const okfRoot = path.join(tmp2, 'okf');
|
|
await fs.access(path.join(okfRoot, 'graph/index.html'));
|
|
assert.ok(
|
|
warnings.some(w => /generateGraphViewer.*deprecated/i.test(w)),
|
|
`expected deprecation warning, got ${JSON.stringify(warnings)}`
|
|
);
|
|
} finally {
|
|
console.log = origLog;
|
|
await fs.rm(tmp2, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('uses empty stats when no pages match', async () => {
|
|
const tmp3 = await fs.mkdtemp(path.join(os.tmpdir(), 'okf-empty-'));
|
|
try {
|
|
await onPostBuild({
|
|
config: { title: 'empty', plugins: { okf: {} } },
|
|
pages: [{ outputPath: 'x.html', frontmatter: { noindex: true }, sourcePath: '', rawMarkdown: '' }],
|
|
outputDir: tmp3,
|
|
log: () => {}
|
|
});
|
|
const yaml = await fs.readFile(path.join(tmp3, 'okf', 'okf.yaml'), 'utf8');
|
|
assert.match(yaml, /concepts: 0/);
|
|
} finally {
|
|
await fs.rm(tmp3, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}); |