fix(audit): 审计问题全量修复(docs/12)

高危逻辑 A1-A10:saved 写盘快照、重载卸载回写闸门与期望值校验、
?file 无条件打开、偏好读后才写+防抖、depth=0 守卫、查找命中随编辑
作废、自动保存避让冲突标签、搜索防链接环、重 IO 命令异步化+上限。
样式:--panel 更正为 --bg-panel、新增 .input 基础样式、文档主题
纸面/字色成对、打印隐藏清单补全、paperTone 驱动源码明暗。
性能:状态栏/大纲按正文 memo、大纲延后提取、Sidebar memo、专注
装饰映射复用、查找防抖、图片 base64 过 IPC。
中危一批:关窗落盘后放行、回收站去向明示、GBK 搜索、create_new
落盘、Mermaid 渲染代次守卫、多行粘贴分段等。
回归:新增 tabkey/audit-fix 集成测试(E1 Tab 抢先定性排除),
vitest 71 通过 + 2 跳过。
This commit is contained in:
Muse
2026-11-13 23:34:19 +08:00
parent 78fbf6a91b
commit db81e60d71
26 changed files with 1064 additions and 228 deletions

102
tests/audit-fix.test.ts Normal file
View File

@@ -0,0 +1,102 @@
// 审计修复回归测试(2026-11-13,对应 docs/11 的高危/中危项)
import { describe, it, expect } from 'vitest'
import { createMarkdownEditor, cursorFromSelection } from '../src/editor/editor'
import { editorViewCtx } from '@milkdown/kit/core'
import { AllSelection, NodeSelection } from '@milkdown/kit/prose/state'
import { buildTextIndex, findInIndex } from '../src/editor/search'
import { insertPlainText } from '../src/editor/paste'
import { findBlockIndex } from '../src/editor/cursorSync'
import { getMarkdown } from '../src/editor/editor'
async function makeEditor(markdown: string) {
const host = document.createElement('div')
document.body.appendChild(host)
const editor = createMarkdownEditor(host, markdown)
await editor.create()
return { host, editor }
}
function docOf(editor: ReturnType<typeof createMarkdownEditor>) {
return editor.action((ctx) => ctx.get(editorViewCtx).state.doc)
}
describe('cursorFromSelection 的 depth=0 选区(审计高危 6)', () => {
it('全选时不抛 RangeError', async () => {
const { host, editor } = await makeEditor('第一段\n\n第二段')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
expect(() => cursorFromSelection(new AllSelection(view.state.doc))).not.toThrow()
})
await editor.destroy()
host.remove()
})
it('选中公式块(NodeSelection)时不抛 RangeError', async () => {
const { host, editor } = await makeEditor('前文\n\n$$\nx=1\n$$\n\n后文')
editor.action((ctx) => {
const view = ctx.get(editorViewCtx)
let mathPos = -1
view.state.doc.descendants((node, pos) => {
if (node.type.name === 'math_block') mathPos = pos
return mathPos < 0
})
expect(mathPos).toBeGreaterThanOrEqual(0)
const info = cursorFromSelection(new NodeSelection(view.state.doc.resolve(mathPos)))
expect(info.line).toBeGreaterThanOrEqual(1)
})
await editor.destroy()
host.remove()
})
})
describe('查找索引修复(审计)', () => {
it('正则命中横跨块间分隔符时被跳过', async () => {
const { host, editor } = await makeEditor('foo\n\nbar baz')
const index = buildTextIndex(docOf(editor) as never)
const cross = findInIndex(index, 'foo\\s+bar', { caseSensitive: false, wholeWord: false, regex: true })
expect(cross).toHaveLength(0)
const within = findInIndex(index, 'bar', { caseSensitive: false, wholeWord: false, regex: false })
expect(within).toHaveLength(1)
await editor.destroy()
host.remove()
})
it('整词对正则模式同样生效', async () => {
const { host, editor } = await makeEditor('hello world hello shell')
const index = buildTextIndex(docOf(editor) as never)
expect(findInIndex(index, 'hello', { caseSensitive: true, wholeWord: true, regex: true })).toHaveLength(2)
expect(findInIndex(index, 'hell', { caseSensitive: true, wholeWord: true, regex: true })).toHaveLength(0)
await editor.destroy()
host.remove()
})
it('含 İ 的文档不区分大小写查找位置不偏移', async () => {
const { host, editor } = await makeEditor('İ x hello')
const index = buildTextIndex(docOf(editor) as never)
const hits = findInIndex(index, 'hello', { caseSensitive: false, wholeWord: false, regex: false })
expect(hits).toHaveLength(1)
// 段落内容在文档中从位置 1 开始,'İ x ' 占 4 个字符,hello 应落在 5
expect(hits[0].from).toBe(5)
await editor.destroy()
host.remove()
})
})
describe('粘贴与锚点修复(审计)', () => {
it('多行纯文本粘贴按段落插入,不塞进单个文本节点', async () => {
const { host, editor } = await makeEditor('开头')
insertPlainText(editor, '第一行\n第二行')
const md = getMarkdown(editor)
expect(md).toContain('第一行')
expect(md).toContain('第二行')
// 两行应成为两个段落(序列化里是空行分隔),而不是一行里的字面换行
expect(md.replace(/\n+$/, '')).toMatch(/第一行\n\n/)
await editor.destroy()
host.remove()
})
it('锚点兜底匹配不被空段落劫持', () => {
const blocks = ['', '目标文本 abc', '其他']
expect(findBlockIndex(blocks, { blockText: '目标文本', offsetInBlock: 0 })).toBe(1)
})
})