Serialize appendEvent sends under the subscriber mutex, wrap worker/memory goroutines with recover, lock synchronous chat SendMessage, and escape KaTeX error HTML. Co-authored-by: Cursor <cursoragent@cursor.com>
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
import { test, describe } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { parseReplyMarkdown } from './parseReplyMarkdown';
|
|
import { renderClipboardHtml } from './renderClipboardHtml';
|
|
import { renderPlainText } from './renderPlainText';
|
|
import { serializeMarkdown } from './serializeMarkdown';
|
|
import { stripUnsafeHtml, sanitizeLinkHref, escapeHtml } from './sanitize';
|
|
import { deriveReplyTitle, sanitizeFilename } from '../../features/replyExport/title';
|
|
|
|
describe('Reply Document IR & Export Engine', () => {
|
|
test('deriveReplyTitle and sanitizeFilename derive title and safe file name', () => {
|
|
const md = '# 医疗器械临床评价报告\n\n本报告基于GB 9706标准进行评价。';
|
|
const title = deriveReplyTitle(md, '2026-09-10 10:00');
|
|
assert.equal(title, '医疗器械临床评价报告');
|
|
const safeName = sanitizeFilename(title);
|
|
assert.equal(safeName, '医疗器械临床评价报告');
|
|
});
|
|
|
|
test('deriveReplyTitle handles text without heading', () => {
|
|
const md = '根据最新医疗器械法规要求,必须建立完整的风险管理体系。';
|
|
const title = deriveReplyTitle(md);
|
|
assert.ok(title.startsWith('根据最新医疗器械法规要求'));
|
|
});
|
|
|
|
test('parseReplyMarkdown parses headers, lists, code, and tables', () => {
|
|
const sample = `
|
|
# 临床评价总结
|
|
|
|
1. 第一项评估
|
|
2. 第二项评估
|
|
|
|
- 无序要点 A
|
|
- 无序要点 B
|
|
|
|
> 这是一个引用说明
|
|
|
|
\`\`\`python
|
|
def evaluate():
|
|
return True
|
|
\`\`\`
|
|
|
|
| 参数 | 阈值 | 结果 |
|
|
| :--- | :---: | ---: |
|
|
| 漏电流 | 10uA | 合格 |
|
|
| 耐压 | 1500V | 合格 |
|
|
`;
|
|
const ir = parseReplyMarkdown(sample);
|
|
assert.equal(ir.title, '临床评价总结');
|
|
assert.ok(ir.nodes.length >= 6);
|
|
|
|
const types = ir.nodes.map((b) => b.type);
|
|
assert.ok(types.includes('heading'));
|
|
assert.ok(types.includes('list'));
|
|
assert.ok(types.includes('blockquote'));
|
|
assert.ok(types.includes('code'));
|
|
assert.ok(types.includes('table'));
|
|
});
|
|
|
|
test('renderClipboardHtml renders self-contained styling for office tools', () => {
|
|
const md = `# 标题
|
|
|
|
正文包含 **粗体** 和 *斜体* 以及 [链接](https://example.com)。
|
|
|
|
- 项目 1
|
|
- 项目 2
|
|
`;
|
|
const ir = parseReplyMarkdown(md);
|
|
const html = renderClipboardHtml(ir);
|
|
|
|
assert.ok(html.includes('<h1'));
|
|
assert.ok(html.includes('style="') || html.includes('<style>'));
|
|
assert.ok(html.includes('font-family:'));
|
|
assert.ok(html.includes('粗体'));
|
|
assert.ok(html.includes('斜体'));
|
|
assert.ok(html.includes('<ul'));
|
|
});
|
|
|
|
test('renderClipboardHtml converts LaTeX formulas to native MathML for Word/Office', () => {
|
|
const md = `关于超声刀振幅计算:
|
|
|
|
$$
|
|
f_0 = \\frac{c}{2L}
|
|
$$
|
|
|
|
其中声速与波长满足关系 $c = \\lambda f$。`;
|
|
const ir = parseReplyMarkdown(md);
|
|
const html = renderClipboardHtml(ir);
|
|
|
|
// 验证包含标准 MathML 命名空间与标签
|
|
assert.ok(html.includes('<math xmlns="http://www.w3.org/1998/Math/MathML"'));
|
|
assert.ok(html.includes('<mfrac>'));
|
|
assert.ok(html.includes('f_0'));
|
|
assert.ok(html.includes('math-block'));
|
|
});
|
|
|
|
test('renderPlainText strips formatting cleanly', () => {
|
|
const md = `# 临床报告
|
|
|
|
正文带 **重点**。
|
|
|
|
| 列1 | 列2 |
|
|
| --- | --- |
|
|
| A | B |
|
|
`;
|
|
const ir = parseReplyMarkdown(md);
|
|
const plain = renderPlainText(ir);
|
|
|
|
assert.ok(plain.includes('临床报告'));
|
|
assert.ok(plain.includes('正文带 重点。'));
|
|
assert.ok(plain.includes('列1\t列2'));
|
|
assert.ok(!plain.includes('**重点**'));
|
|
});
|
|
|
|
test('serializeMarkdown round-trips to valid markdown', () => {
|
|
const md = `# 临床报告
|
|
|
|
正文说明。
|
|
|
|
- 要点 1
|
|
- 要点 2
|
|
`;
|
|
const ir = parseReplyMarkdown(md);
|
|
const serialized = serializeMarkdown(ir);
|
|
|
|
assert.ok(serialized.includes('# 临床报告'));
|
|
assert.ok(serialized.includes('正文说明。'));
|
|
assert.ok(serialized.includes('- 要点 1'));
|
|
});
|
|
|
|
test('stripUnsafeHtml strips script tags and dangerous event handlers', () => {
|
|
const risky = '<p>Normal</p><script>alert(1)</script><a href="javascript:steal()" onclick="alert(2)">Link</a>';
|
|
const clean = stripUnsafeHtml(risky);
|
|
|
|
assert.ok(!clean.includes('<script>'));
|
|
assert.ok(!clean.includes('alert(1)'));
|
|
assert.ok(!clean.includes('onclick'));
|
|
assert.ok(clean.includes('Normal'));
|
|
assert.equal(sanitizeLinkHref('javascript:alert(1)'), '#');
|
|
});
|
|
|
|
test('escapeHtml escapes math error payloads used by BlockEditorModal catch path', () => {
|
|
const dirty = `<img src=x onerror=alert(1)><script>alert(2)</script>`;
|
|
const escaped = escapeHtml(dirty);
|
|
assert.ok(!escaped.includes('<script>'));
|
|
assert.ok(!escaped.includes('<img'));
|
|
assert.ok(escaped.includes('<script>'));
|
|
assert.ok(escaped.includes('<img'));
|
|
});
|
|
});
|