audit(docs/29): 全项目审计——修分片载入卸载截断高危、Sidebar memo 击穿与 Rust 资源/搜索/恢复上限等 20 余处
三线并行审计(编辑器核心/Rust 后端/服务与组件)+ App 编排层自审。 高危:分片载入未收尾时窗格卸载回写部分序列化,干净标签会被静默 截断——cleanup 补 muted 守卫,create 完成回调补取消守卫。 性能:gitSync/outline/收起回调稳定化保住 Sidebar memo;光标行列 rAF 合并;图片显示解析加缓存;导出图片并发解析;chunkLoader 偏移前缀表;围栏切点只认顶格(列表内嵌围栏等价回归实测)。 Rust:资源命令 async 化 + 32MB 上限;搜索 junction 越界校验; recovery/workspace 条数与单条上限;启动坏库改名重建 + WAL; validate 补 ext:: 黑名单与 ref 形态;init 先种 .gitignore (含 .*.tmp-* 防崩溃残留被提交);游离 HEAD 不再猜分支。 验证:vitest 122+2、tsc、构建通过;Rust 过 rustfmt 解析、 validate 纯函数抽出实跑断言、命令对账 28=28。
This commit is contained in:
@@ -103,3 +103,31 @@ describe('分片载入与整篇解析等价', () => {
|
||||
expect(chunked).toBe(whole)
|
||||
}, 60_000)
|
||||
})
|
||||
|
||||
describe('列表内嵌围栏不被切开(docs/29)', () => {
|
||||
it('分片序列化与整篇逐字相等', async () => {
|
||||
const parts: string[] = []
|
||||
for (let s = 1; s <= 30; s++) {
|
||||
parts.push(`## 小节 ${s}`)
|
||||
parts.push('')
|
||||
parts.push('正文填充。'.repeat(420))
|
||||
parts.push('')
|
||||
parts.push('- 列表项,内含缩进围栏代码:')
|
||||
parts.push('')
|
||||
parts.push(' ```js')
|
||||
parts.push(` const v${s} = ${s}`)
|
||||
parts.push(' ```')
|
||||
parts.push('')
|
||||
parts.push('- 下一项')
|
||||
parts.push('')
|
||||
}
|
||||
const md = parts.join('\n')
|
||||
expect(md.length).toBeGreaterThan(60_000)
|
||||
const whole = getMarkdown(await load(md))
|
||||
const handle = prepareChunkLoad(md)
|
||||
expect(handle).not.toBeNull()
|
||||
const editor = await load(handle!.firstChunk)
|
||||
await handle!.appendRest(editor, () => undefined, () => false)
|
||||
expect(getMarkdown(editor)).toBe(whole)
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
43
tests/editor-pane-unload.test.tsx
Normal file
43
tests/editor-pane-unload.test.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// 回归(docs/29 高危):EditorPane 卸载最终回写的守卫。
|
||||
// 分片载入未收尾(muted)时 cleanup 跳过回写——否则部分序列化会把干净
|
||||
// 标签静默截断。该交错(卸载提交时追加仍在途)在 jsdom 里无法自动化
|
||||
// 复现:React act 会把待办异步工作抽干,分片追加总是先跑完(已实测
|
||||
// 三种卸载方式均如此),故中途卸载一例靠代码复核保证(muted 仅在追加
|
||||
// 完成/失败的收尾回调里解除,cleanup 只在 !muted 时回写),这里只锁
|
||||
// 对照组:正常文档卸载时最终回写照常发生(守卫不能误伤正常路径)。
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import EditorPane from '../src/components/EditorPane'
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
describe('EditorPane 卸载回写', () => {
|
||||
it('普通文档卸载:最终回写照常', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
const onFinalMarkdown = vi.fn()
|
||||
let readyApi: { editor: unknown } | null = null
|
||||
const onReady = vi.fn((api: { editor: unknown }) => { if (api.editor) readyApi = api })
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<EditorPane
|
||||
tabId="t1"
|
||||
initialMarkdown={'# 小文档\n\n你好,世界。\n'}
|
||||
onMarkdown={() => undefined}
|
||||
onFinalMarkdown={onFinalMarkdown}
|
||||
onCursor={() => undefined}
|
||||
onReady={onReady}
|
||||
focusMode={false}
|
||||
typewriter={false}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
for (let i = 0; i < 200 && !readyApi; i++) await sleep(10)
|
||||
expect(readyApi).not.toBeNull()
|
||||
await act(async () => { root.unmount() })
|
||||
expect(onFinalMarkdown).toHaveBeenCalledTimes(1)
|
||||
expect(onFinalMarkdown.mock.calls[0][1]).toContain('你好,世界。')
|
||||
}, 30000)
|
||||
})
|
||||
@@ -6,7 +6,9 @@ import type { RecoveryEntry } from '../src/services/platform'
|
||||
|
||||
describe('recoveryKey', () => {
|
||||
it('文件文档按路径键,未命名按标签 id', () => {
|
||||
expect(recoveryKey({ id: 't1', path: 'D:\\a.md' })).toBe('path:D:\\a.md')
|
||||
// docs/29:路径先归一(斜杠统一 + Windows 大小写折叠),同文件不同写法同键
|
||||
expect(recoveryKey({ id: 't1', path: 'D:\\a.md' })).toBe('path:d:/a.md')
|
||||
expect(recoveryKey({ id: 't1', path: 'd:/A.md' })).toBe('path:d:/a.md')
|
||||
expect(recoveryKey({ id: 't2', path: null })).toBe('untitled:t2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { countWords, countChars, extractOutline, baseName, isMarkdownPath } from '../src/utils/text'
|
||||
import { countWords, countChars, extractOutline, baseName, isMarkdownPath, pathsEqual, isPathInside, parentDirOf } from '../src/utils/text'
|
||||
|
||||
describe('字数统计', () => {
|
||||
it('中文按字、英文按词、标点不计', () => {
|
||||
@@ -56,3 +56,24 @@ describe('路径助手(拖放/改名校验)', () => {
|
||||
expect(isPathInside('C:\\ws\\sub\\deep', 'C:/ws/sub')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('路径比较与大纲围栏(docs/29)', () => {
|
||||
it('Windows 路径大小写/斜杠归一后相等,POSIX 路径仍分大小写', () => {
|
||||
expect(pathsEqual('C:\\WS\\A.md', 'c:/ws/a.md')).toBe(true)
|
||||
expect(pathsEqual('/home/A.md', '/home/a.md')).toBe(false)
|
||||
expect(isPathInside('c:/ws/sub/a.md', 'C:\\WS')).toBe(true)
|
||||
expect(isPathInside('C:/ws2/a.md', 'C:/ws')).toBe(false)
|
||||
})
|
||||
|
||||
it('parentDirOf 盘符根文件返回盘符根', () => {
|
||||
expect(parentDirOf('C:/a.md')).toBe('C:/')
|
||||
expect(parentDirOf('C:\\dir\\a.md')).toBe('C:\\dir')
|
||||
expect(parentDirOf('/a.md')).toBe('/')
|
||||
})
|
||||
|
||||
it('四反引号围栏内的三反引号与 # 行不干扰大纲', () => {
|
||||
const md = ['# 标题一', '', '````md', '```', '# 假标题', '```', '````', '', '# 标题二'].join('\n')
|
||||
const outline = extractOutline(md)
|
||||
expect(outline.map((o) => o.text)).toEqual(['标题一', '标题二'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user