147 lines
6.3 KiB
JavaScript
147 lines
6.3 KiB
JavaScript
/**
|
|
* --------------------------------------------------------------------
|
|
* docmd : the zero-config documentation engine.
|
|
*
|
|
* Slice C.3 — Public API additions
|
|
*
|
|
* Covers:
|
|
* D-H4 createSourceTools.getBlocks(file) — enumerate all top-level
|
|
* blocks in a file (paragraph-level split on blank lines).
|
|
* D-H6 Live editor uses safePath from @docmd/api to resolve the
|
|
* requested URL against the public dir, with a 403 fallback on
|
|
* path-escape attempts.
|
|
* D-M3 onBeforeRender and onPageReady receive the same page shape
|
|
* (urlContext + config are now both included in onBeforeRender).
|
|
* D-M4 The duplicate safePath in packages/api/src/source.ts is gone —
|
|
* we now import the canonical one from @docmd/utils.
|
|
*
|
|
* Run: `node tests/runner.js --only=source-tools`
|
|
* --------------------------------------------------------------------
|
|
*/
|
|
|
|
import {
|
|
DOCMD,
|
|
setup,
|
|
writeFile,
|
|
runTestFile
|
|
} from '../shared.js';
|
|
import { execSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const failures = [];
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
failed++;
|
|
failures.push(message);
|
|
console.log(` ❌ ${message}`);
|
|
} else {
|
|
passed++;
|
|
console.log(` ✅ ${message}`);
|
|
}
|
|
}
|
|
|
|
export const test = runTestFile({
|
|
name: 'Source tools + page shape consistency (Slice C.3)',
|
|
emoji: '🧰',
|
|
run: async () => {
|
|
|
|
// D-H4 — createSourceTools exposes a getBlocks(file) entry point.
|
|
// Source tools are a runtime concept (the live editor hands a tools
|
|
// instance to plugins via WebSocket RPC), so we test the surface via
|
|
// direct import + invocation. The tool is created with a small
|
|
// fixture and getBlocks is called against it.
|
|
{
|
|
const tmpDir = setup('source-tools-c3-dh4-getblocks');
|
|
writeFile(tmpDir, 'docs/index.md', [
|
|
'# Heading 1',
|
|
'',
|
|
'First paragraph block.',
|
|
'',
|
|
'Second paragraph block.',
|
|
'',
|
|
'- List item one',
|
|
'- List item two',
|
|
'',
|
|
'Third block after the list.',
|
|
''
|
|
].join('\n'));
|
|
|
|
// Import the helper directly. We use a relative path to the
|
|
// monorepo so the import resolves without needing a node_modules
|
|
// install in the fixture.
|
|
const apiPath = path.resolve(import.meta.dirname, '..', '..', 'packages', 'api', 'dist', 'index.js');
|
|
const { createSourceTools } = await import(apiPath);
|
|
const tools = createSourceTools({ projectRoot: tmpDir });
|
|
const blocks = await tools.getBlocks('docs/index.md');
|
|
|
|
// 5 blocks: heading, p1, p2, list (multi-line), p3.
|
|
assert(blocks.length === 5, 'D-H4: getBlocks returns 5 blocks (heading + 2 paragraphs + list + 3rd paragraph)');
|
|
const starts = blocks.map((b) => b.line.start).join(',');
|
|
assert(/^0,2,4,6,9$/.test(starts), 'D-H4: block line.start values match the blank-line-split boundaries');
|
|
}
|
|
|
|
// D-M3 — onBeforeRender and onPageReady receive the same page
|
|
// shape. We inspect the generator source for the construction of
|
|
// both pageContext objects and assert the key sets match.
|
|
{
|
|
const genSrc = fs.readFileSync(
|
|
path.resolve(import.meta.dirname, '..', '..', 'packages', 'core', 'src', 'engine', 'generator.ts'),
|
|
'utf8'
|
|
);
|
|
// The onBeforeRender pageContext block (Phase 3A) must include
|
|
// `urlContext` and `config` keys. This is the D-M3 fix.
|
|
const beforeMatch = genSrc.match(/Phase 3A[\s\S]*?const pageContext = \{([\s\S]*?)\};[\s\S]*?onBeforeRender/);
|
|
assert(beforeMatch && /urlContext/.test(beforeMatch[1]), 'D-M3: onBeforeRender pageContext includes urlContext');
|
|
assert(beforeMatch && /\bconfig\b/.test(beforeMatch[1]), 'D-M3: onBeforeRender pageContext includes config');
|
|
// The onPageReady pageObj is declared right before the loop. Look
|
|
// for `const pageObj = {` followed by the loop.
|
|
const readyMatch = genSrc.match(/const pageObj = \{([\s\S]*?)\};[\s\S]*?for \(const fn of hooks\.onPageReady\)/);
|
|
assert(readyMatch && /urlContext/.test(readyMatch[1]), 'D-M3: onPageReady pageObj includes urlContext');
|
|
assert(readyMatch && /\bconfig\b/.test(readyMatch[1]), 'D-M3: onPageReady pageObj includes config');
|
|
}
|
|
|
|
// D-H6 — the live editor's static-file handler uses the canonical
|
|
// safePath and 403s on path-escape attempts. We test this by
|
|
// directly importing the live editor's exported server and issuing
|
|
// a request with a traversal attempt.
|
|
{
|
|
// The live editor is a CLI binary; testing it through the binary
|
|
// would require a running server. Instead, we unit-test the
|
|
// safePath dependency: the live editor's `index.ts` imports
|
|
// `safePath` from `@docmd/api` (this is the contract we want to
|
|
// preserve). The previous path was `path.join` + ad-hoc stripper
|
|
// which happened to be safe by accident.
|
|
const liveIndex = fs.readFileSync(
|
|
path.resolve(import.meta.dirname, '..', '..', 'packages', 'live', 'src', 'index.ts'),
|
|
'utf8'
|
|
);
|
|
assert(/import \{ safePath as canonicalSafePath \} from .@docmd\/api.;/.test(liveIndex), 'D-H6: live editor imports safePath from @docmd/api');
|
|
// The old `path.join(publicDir, safePath)` is gone — the canonical
|
|
// safePath call replaces it.
|
|
assert(!/path\.join\(publicDir, safePath\)/.test(liveIndex), 'D-H6: live editor no longer uses raw path.join(publicDir, safePath)');
|
|
}
|
|
|
|
// D-M4 — packages/api/src/source.ts no longer has a local
|
|
// safePath duplicate; it imports the canonical one from
|
|
// @docmd/utils (which @docmd/api re-exports).
|
|
{
|
|
const sourceSrc = fs.readFileSync(
|
|
path.resolve(import.meta.dirname, '..', '..', 'packages', 'api', 'src', 'source.ts'),
|
|
'utf8'
|
|
);
|
|
assert(/import\s*\{\s*safePath\s*\}\s*from\s*'@docmd\/utils'/.test(sourceSrc), 'D-M4: source.ts imports safePath from @docmd/utils');
|
|
// No local `function safePath` declaration in source.ts.
|
|
assert(!/^function\s+safePath\s*\(/.test(sourceSrc), 'D-M4: source.ts no longer declares a local safePath');
|
|
}
|
|
}
|
|
});
|
|
|
|
export const results = {
|
|
get passed() { return passed; },
|
|
get failed() { return failed; },
|
|
get failures() { return [...failures]; }
|
|
}; |