Files
wehub-resource-sync 6db8fca185
docmd CI verification / verify (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:55 +08:00

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]; }
};