import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import nodePath from 'node:path'; import os from 'node:os'; import { promises as fs } from 'node:fs'; import { FileService } from '../../apps/desktop/src/main/services/file-service'; import { ConfigStore } from '../../apps/desktop/src/main/services/config-store'; const tmp = nodePath.join(os.tmpdir(), 'dsh-file-service-test'); beforeEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); await fs.mkdir(tmp, { recursive: true }); }); afterEach(async () => { await fs.rm(tmp, { recursive: true, force: true }); }); describe('FileService CRUD (FR-F)', () => { it('create 文档生成 .md', async () => { const s = new FileService(tmp); const { path } = await s.create('', '笔记', false); expect(path).toBe('笔记.md'); expect(await fs.readFile(nodePath.join(tmp, '笔记.md'), 'utf8')).toBe(''); }); it('create 文件夹', async () => { const s = new FileService(tmp); const { path } = await s.create('', '目录', true); expect(path).toBe('目录'); expect((await fs.stat(nodePath.join(tmp, '目录'))).isDirectory()).toBe(true); }); it('rename 文档自动补 .md 并去重', async () => { const s = new FileService(tmp); await s.create('', 'a', false); await s.create('', 'b', false); const { path } = await s.rename('b.md', 'a'); expect(path).toBe('a-1.md'); }); it('move 到目标目录', async () => { const s = new FileService(tmp); await s.create('', 'dir', true); await s.create('', 'x', false); const { path } = await s.move('x.md', 'dir'); expect(path).toBe('dir/x.md'); }); it('move 禁止移入自身内部', async () => { const s = new FileService(tmp); await s.create('', 'dir', true); await s.create('', 'dir/sub', true); await expect(s.move('dir', 'dir/sub')).rejects.toThrow(); }); it('delete 文档与递归文件夹', async () => { const s = new FileService(tmp); await s.create('', 'f', true); await s.create('f', 'inner', false); await s.delete('f', true); await expect(fs.stat(nodePath.join(tmp, 'f'))).rejects.toThrow(); }); it('listTree 排序:文件夹在前、忽略隐藏目录', async () => { const s = new FileService(tmp); await s.create('', 'zdoc', false); await s.create('', 'afolder', true); await s.create('', '.hidden', true); await s.create('afolder', 'in', false); const tree = await s.listTree(); const folders = tree.filter(n => n.type === 'folder').map(n => n.name); const docs = tree.filter(n => n.type === 'doc' && n.parent === '').map(n => n.name); expect(folders[0]).toBe('afolder'); expect(docs).toContain('zdoc'); expect(folders).not.toContain('.hidden'); }); it('importFiles 复制并去重,返回成功与失败', async () => { const s = new FileService(tmp); const ext = nodePath.join(tmp, '_external'); await fs.mkdir(ext, { recursive: true }); await fs.writeFile(nodePath.join(ext, 'p.md'), 'hi'); const r = await s.importFiles([nodePath.join(ext, 'p.md'), nodePath.join(ext, 'nope.md')], ''); expect(r.ok).toEqual(['p.md']); expect(r.failed.length).toBe(1); }); }); describe('FileService 金标准:writeDoc → readDoc 往返字节一致 (FR-S5/FR-M1)', () => { it('纯正文往返', async () => { const s = new FileService(tmp); await s.create('', 'doc', false); const body = '# 标题\n\n段落含 **粗** 与 `code`。'; await s.writeDoc({ path: 'doc.md', meta: {}, body }); const r = await s.readDoc('doc.md'); expect(r.body).toBe(body); expect(r.meta).toEqual({}); }); it('含 frontmatter 往返', async () => { const s = new FileService(tmp); await s.create('', 'doc', false); const meta = { title: '标题', tags: ['a','b'], star: true, links: ['x/y.md'] }; const body = '# 正文\n内容'; await s.writeDoc({ path: 'doc.md', meta, body }); const r = await s.readDoc('doc.md'); expect(r.meta).toEqual(meta); expect(r.body).toBe(body); }); it('重开后内容仍一致(模拟重启)', async () => { const body = '持久化内容\n- 项一\n- 项二'; await (new FileService(tmp)).create('', 'persist', false); await (new FileService(tmp)).writeDoc({ path: 'persist.md', meta: { star: true }, body }); // 新实例(模拟重启)读取 const r = await (new FileService(tmp)).readDoc('persist.md'); expect(r.body).toBe(body); expect(r.meta.star).toBe(true); }); }); describe('ConfigStore', () => { it('默认空 meta', async () => { const cs = new ConfigStore(tmp); const m = await cs.getVaultMeta(); expect(m.currentRoot).toBe(''); expect(m.recents).toEqual([]); }); it('setCurrentRoot 记录最近库并去重截断', async () => { const cs = new ConfigStore(tmp); await cs.setCurrentRoot('C:/a'); await cs.setCurrentRoot('C:/b'); await cs.setCurrentRoot('C:/a'); const m1 = await cs.getVaultMeta(); expect(m1.currentRoot).toBe('C:/a'); expect(m1.recents).toEqual(['C:/a', 'C:/b']); // 超出上限截断 for (let i = 0; i < 8; i++) await cs.setCurrentRoot('C:/n' + i); const m2 = await cs.getVaultMeta(); expect(m2.recents.length).toBeLessThanOrEqual(5); }); it('重启后持久化(新实例读取)', async () => { await (new ConfigStore(tmp)).setDefaultRoot('C:/default'); const m = await (new ConfigStore(tmp)).getVaultMeta(); expect(m.defaultRoot).toBe('C:/default'); }); });