| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566 |
- import { afterAll, describe, expect, it } from 'vitest'
- import * as fs from 'node:fs'
- import * as path from 'node:path'
- import * as os from 'node:os'
- import { writeBatchAtomic } from '../src/repo/atomic'
- const roots: string[] = []
- function mkRoot(): string {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-b1-'))
- roots.push(dir)
- return dir
- }
- afterAll(() => { for (const r of roots) { try { fs.rmSync(r, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) } catch { /* Windows 句柄延迟释放,%TEMP% 残留无害 */ } } })
- describe('批量原子写(机制 B1)', () => {
- it('全部成功:目录自动创建,内容正确', () => {
- const root = mkRoot()
- writeBatchAtomic(root, [
- { relPath: '定稿/卷01/0001-初见.md', content: '正文' },
- { relPath: '账本/线索.md', content: '账' },
- ])
- expect(fs.readFileSync(path.join(root, '定稿/卷01/0001-初见.md'), 'utf-8')).toBe('正文')
- expect(fs.readFileSync(path.join(root, '账本/线索.md'), 'utf-8')).toBe('账')
- })
- it('路径校验:拒绝绝对路径与 .. 穿越(B1/B2 衔接)', () => {
- const root = mkRoot()
- expect(() => writeBatchAtomic(root, [{ relPath: '../越界.md', content: 'x' }])).toThrow(/B1/)
- expect(() => writeBatchAtomic(root, [{ relPath: 'D:/绝对.md', content: 'x' }])).toThrow(/绝对/)
- expect(fs.readdirSync(root)).toEqual([])
- })
- it('中途失败:新建文件回滚删除,书仓零半成品', () => {
- const root = mkRoot()
- // 预置一个目录,使第二条目标标在 rename 阶段失败(目录不可作文件覆盖)
- fs.mkdirSync(path.join(root, '占位目录'))
- expect(() => writeBatchAtomic(root, [
- { relPath: 'a/新文件.md', content: '新' },
- { relPath: '占位目录', content: 'x' },
- ])).toThrow(/B1/)
- expect(fs.existsSync(path.join(root, 'a/新文件.md'))).toBe(false)
- expect(fs.statSync(path.join(root, '占位目录')).isDirectory()).toBe(true) // 既有目录不受损
- })
- it('中途失败:既有文件恢复原文,零丢失', () => {
- const root = mkRoot()
- fs.mkdirSync(path.join(root, 'a'), { recursive: true })
- fs.writeFileSync(path.join(root, 'a/旧.md'), '旧内容', 'utf-8')
- fs.mkdirSync(path.join(root, '占位目录'))
- expect(() => writeBatchAtomic(root, [
- { relPath: 'a/旧.md', content: '新内容' },
- { relPath: '占位目录', content: 'x' },
- ])).toThrow(/B1/)
- expect(fs.readFileSync(path.join(root, 'a/旧.md'), 'utf-8')).toBe('旧内容')
- })
- it('中途失败:中文路径新建文件也回滚删除', () => {
- const root = mkRoot()
- fs.mkdirSync(path.join(root, '占位目录'))
- expect(() => writeBatchAtomic(root, [
- { relPath: '账本/线索.md', content: '账' },
- { relPath: '占位目录', content: 'x' },
- ])).toThrow(/B1/)
- expect(fs.existsSync(path.join(root, '账本/线索.md'))).toBe(false)
- })
- })
|