update
This commit is contained in:
115
tests/unit/main/notes-name-manageable.test.ts
Normal file
115
tests/unit/main/notes-name-manageable.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
// tests/unit/main/notes-name-manageable.test.ts
|
||||
//
|
||||
// 守 r195「侧栏列得出来的笔记,应用也得管得了」—— 字符集这一层的
|
||||
// 不变式 I2(与 r186 在文件夹层修的同一个洞)。
|
||||
//
|
||||
// 现象(用户可复现):
|
||||
// 1. 用户从 Obsidian / 网盘同步 / 同事的机器上拷来一批 .md,名字里带
|
||||
// '&' '+' '=' '%' "'" '#' '.' 之类(「C++ 学习.md」「v1.2 草稿.md」
|
||||
// 「A&B.md」「100%完成.md」……);
|
||||
// 2. 放进工作区的 notes/ 目录 → 侧栏**照常列出来**(collectNotePaths
|
||||
// 只按后缀收,不做字符过滤);
|
||||
// 3. 但把**同样的文件**拖进应用导入 → ValidationError「文件名包含非法
|
||||
// 字符」⇒ **文件根本进不来**;想把笔记改名成这种名字 → 同样被拒。
|
||||
// ⇒ 「看得见、管不了」—— 与用户诉求「列表 == 文件夹、文件操作要对得上」
|
||||
// 直接冲突。
|
||||
//
|
||||
// 根因:notes 域的文件名校验是一条**白名单 regex**(只收字母/数字/下划线/
|
||||
// 连字符/空格/圆括号/中文),而文献域(literature/rename-file.ts)只拒
|
||||
// Win32 真正不能用的 `< > : " | ? *` + 控制字符 ⇒ 同一个字符,一边行
|
||||
// 一边不行(两侧不对齐)。
|
||||
//
|
||||
// 本测锁(行为级,直接对着磁盘事实说话):
|
||||
// 1. 这些真实存在的文件名**必须被 scanNotes 列出来**(列表侧);
|
||||
// 2. 同一个名字**必须能通过 validator**(⇒ 可导入 / 可改名)—— 两边一致;
|
||||
// 3. 真的建不出来的形态(Win32 保留字符 / 控制字符 / '..' 段 / 隐藏段 /
|
||||
// 保留设备名 / 超长)**仍然被拒**(不能因为放宽就把洞打开)。
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
import { scanNotes } from '../../../src/main/notes/standalone';
|
||||
import { normalizeAndValidateNoteFilename } from '../../../src/main/notes/filename-validator';
|
||||
import { WORKSPACE_DATA_LAYOUT } from '../../../src/shared/types';
|
||||
import { stripExt } from '../../../src/shared/path-utils';
|
||||
|
||||
// 真实世界里极常见的学术笔记名 —— 旧白名单 regex 一个都过不了。
|
||||
const TRICKY_NAMES = [
|
||||
'A&B.md',
|
||||
'C++ 学习.md',
|
||||
'v1.2 草稿.md',
|
||||
'100%完成.md',
|
||||
"O'Brien 访谈.md",
|
||||
'结果=最终.md',
|
||||
'2024-06-01 会议 #3.md',
|
||||
'数据+分析.md',
|
||||
'Q&A 记录.md',
|
||||
'论文(终稿).md',
|
||||
];
|
||||
|
||||
describe('笔记文件名:列得出来就得管得了(r195)', () => {
|
||||
let root: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await fs.mkdtemp(join(tmpdir(), 'rkms-notes-name-'));
|
||||
await fs.mkdir(join(root, WORKSPACE_DATA_LAYOUT.NOTES_DIR), { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('列在侧栏里的名字,必须同时能通过导入 / 改名校验', async () => {
|
||||
for (const name of TRICKY_NAMES) {
|
||||
await fs.writeFile(join(root, WORKSPACE_DATA_LAYOUT.NOTES_DIR, name), '# body\n', 'utf-8');
|
||||
}
|
||||
|
||||
// 1) 列表侧:全部列出来
|
||||
const notes = await scanNotes(root);
|
||||
const listed = new Set(notes.map((n) => n.filename));
|
||||
for (const name of TRICKY_NAMES) {
|
||||
expect(listed.has(name), `${name} 应当被 scanNotes 列出来`).toBe(true);
|
||||
}
|
||||
|
||||
// 2) 操作侧:同一个名字必须能过 validator(导入走 field='filename',
|
||||
// 改名走 field='newFilename' —— 两个入口都要过)
|
||||
for (const name of TRICKY_NAMES) {
|
||||
const stem = stripExt(name);
|
||||
expect(() => normalizeAndValidateNoteFilename(stem, 'filename')).not.toThrow();
|
||||
expect(() => normalizeAndValidateNoteFilename(stem, 'newFilename')).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('建不出来的形态仍然被拒(放宽字符集不能把洞打开)', () => {
|
||||
const bad: Array<[string, 'filename' | 'newFilename']> = [
|
||||
['a:b.md', 'filename'], // Win32 保留字符
|
||||
['a*b.md', 'filename'],
|
||||
['a?b.md', 'filename'],
|
||||
['a<b.md', 'filename'],
|
||||
['a>b.md', 'filename'],
|
||||
['a|b.md', 'filename'],
|
||||
['a"b.md', 'filename'],
|
||||
['a\\b.md', 'filename'], // 反斜杠 = 路径分隔符
|
||||
['a\u0001b.md', 'filename'], // 控制字符
|
||||
['../evil.md', 'filename'], // 路径穿越
|
||||
['a/../evil.md', 'filename'],
|
||||
['a//b.md', 'filename'], // 空段
|
||||
['.hidden.md', 'filename'], // 隐藏项(collectNotePaths 也跳过)
|
||||
['sub./a.md', 'filename'], // Win32 静默截断
|
||||
['sub /a.md', 'filename'],
|
||||
['con.md', 'filename'], // Windows 保留设备名
|
||||
['con.txt.md', 'filename'], // 词干是 con
|
||||
['com1.md', 'filename'],
|
||||
];
|
||||
for (const [name, field] of bad) {
|
||||
expect(() => normalizeAndValidateNoteFilename(name, field), name).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('改名只改文件名、不改位置(r175 不变式):名字里不许带路径分隔符', () => {
|
||||
expect(() => normalizeAndValidateNoteFilename('sub/x.md', 'newFilename')).toThrow();
|
||||
// 新建到子目录仍然允许 '/' (create 路径靠它)
|
||||
expect(normalizeAndValidateNoteFilename('sub/x.md', 'filename')).toBe('sub/x.md');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user