- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
51 lines
2.3 KiB
JavaScript
51 lines
2.3 KiB
JavaScript
// @vitest-environment jsdom
|
||
// markdown.test.js —— Markdown 渲染 + DOMPurify 消毒 + 自定义表格/代码类(FR-CHAT-004)
|
||
import { describe, it, expect } from 'vitest';
|
||
import { renderMarkdown, NON_MONO_HEADER } from '../../../../ui/src/components/chat/markdown.js';
|
||
|
||
describe('renderMarkdown(FR-CHAT-004)', () => {
|
||
it('LLM 输出 markdown → 消毒 HTML', () => {
|
||
const html = renderMarkdown('**加粗** 与 *斜体* 与 `行内代码`');
|
||
expect(html).toContain('<strong>加粗</strong>');
|
||
expect(html).toContain('<em>斜体</em>');
|
||
expect(html).toContain('<code>行内代码</code>');
|
||
});
|
||
|
||
it('脚本注入被消毒(活 DOM 中无可执行元素/属性/协议)', () => {
|
||
const html = renderMarkdown('你好 <script>alert(1)</script> <img src=x onerror="alert(2)"> [x](javascript:alert(3))');
|
||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||
expect(doc.querySelectorAll('script').length).toBe(0);
|
||
expect(doc.querySelectorAll('img').length).toBe(0);
|
||
expect(doc.querySelectorAll('[onerror]').length).toBe(0);
|
||
const hrefs = [...doc.querySelectorAll('a')].map((a) => a.getAttribute('href') || '');
|
||
expect(hrefs.filter((h) => h.toLowerCase().startsWith('javascript:'))).toEqual([]);
|
||
});
|
||
|
||
it('表格挂 md-table 类(thead th / tbody td)', () => {
|
||
const html = renderMarkdown('| A | B |\n|---|---|\n| 1 | 2 |');
|
||
expect(html).toContain('<table class="md-table">');
|
||
expect(html).toContain('<thead>');
|
||
expect(html).toContain('<th>A</th>');
|
||
expect(html).toContain('<tbody>');
|
||
});
|
||
|
||
it('代码块挂 code-block 类并转义内容', () => {
|
||
const html = renderMarkdown('```\nconst x = "<script>";\n```');
|
||
expect(html).toContain('<div class="code-block">');
|
||
expect(html).toContain('<script>');
|
||
});
|
||
|
||
it('渲染失败时回退转义原文,不抛异常、活 DOM 无事件属性', () => {
|
||
const html = renderMarkdown(null);
|
||
expect(typeof html).toBe('string');
|
||
const evil = renderMarkdown('<b onclick=alert(1)>x</b>');
|
||
const doc = new DOMParser().parseFromString(evil, 'text/html');
|
||
expect(doc.querySelectorAll('[onclick]').length).toBe(0);
|
||
expect(evil).not.toContain('<b onclick');
|
||
});
|
||
|
||
it('NON_MONO_HEADER 常量为「说明」', () => {
|
||
expect(NON_MONO_HEADER).toBe('说明');
|
||
});
|
||
});
|