Files
Notes/tests/unit/generate-tmp-path.test.js
2026-10-16 16:34:15 +08:00

97 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// main/file-ops.js atomicWriteFile —— tmp 路径生成(audit P2-1)
//
// 背景:原 inline 实现 `Math.random().toString(36).slice(2, 8)`
// - Math.random() 不加密安全,并发同毫秒 + 同 pid + 同 dst 可能撞名
// - audit 要求与 crypto.randomUUID() 对齐(已在本仓库其它路径使用)
//
// 修复:抽 generateTmpPath(dst, { crypto, pid, now }) 工厂:
// - 注入 crypto / pid / now 便于单测验证随机性来源
// - 默认走 node:crypto.randomUUID()(128-bit 加密安全,6 段 8-4-4-4-12 hex)
// - 路径格式:${dst}.tmp.${pid}.${ts}.${randomId}(保持现有格式不破坏 fs-watcher 兼容)
//
// 测试策略:纯函数 + 注入 crypto / pid / now。覆盖:
// - crypto.randomUUID 被调用(mock)
// - 同 pid+now 两次调用 → randomId 不同(实际是不同 UUID)
// - 不同 pid 反映在路径里
// - 不同 now 反映在路径里
// - 路径格式完整:dst + ".tmp." + pid + "." + ts + "." + randomId
import { describe, it, expect, vi } from 'vitest';
import { generateTmpPath } from '../../main/generate-tmp-path.js';
describe('generateTmpPath(audit P2-1)', () => {
it('crypto.randomUUID 被调用,返回的 randomId 来自它', () => {
const cryptoMock = { randomUUID: vi.fn(() => 'uuid-abc-123') };
const r = generateTmpPath('/data/note.md', {
crypto: cryptoMock,
pid: 1000,
now: () => 1234567890,
});
expect(cryptoMock.randomUUID).toHaveBeenCalledTimes(1);
expect(r).toBe('/data/note.md.tmp.1000.1234567890.uuid-abc-123');
});
it('同 pid+now 但 crypto 每次返不同 UUID → 路径不同', () => {
let counter = 0;
const cryptoMock = { randomUUID: vi.fn(() => `uuid-${++counter}`) };
const r1 = generateTmpPath('/data/note.md', { crypto: cryptoMock, pid: 1000, now: () => 1234 });
const r2 = generateTmpPath('/data/note.md', { crypto: cryptoMock, pid: 1000, now: () => 1234 });
expect(r1).not.toBe(r2);
expect(r1).toContain('uuid-1');
expect(r2).toContain('uuid-2');
});
it('不同 pid 反映在路径里', () => {
const cryptoMock = { randomUUID: vi.fn(() => 'uuid-x') };
const r1 = generateTmpPath('/data/a.md', { crypto: cryptoMock, pid: 100, now: () => 1234 });
const r2 = generateTmpPath('/data/a.md', { crypto: cryptoMock, pid: 200, now: () => 1234 });
expect(r1).toContain('.tmp.100.');
expect(r2).toContain('.tmp.200.');
});
it('不同 now 反映在路径里', () => {
const cryptoMock = { randomUUID: vi.fn(() => 'uuid-x') };
const r1 = generateTmpPath('/data/a.md', { crypto: cryptoMock, pid: 1, now: () => 1000 });
const r2 = generateTmpPath('/data/a.md', { crypto: cryptoMock, pid: 1, now: () => 2000 });
expect(r1).toContain('.1.1000.uuid-x');
expect(r2).toContain('.1.2000.uuid-x');
});
it('路径格式:${dst}.tmp.${pid}.${ts}.${randomId},5 段以 . 分割', () => {
const r = generateTmpPath('/data/note.md', {
crypto: { randomUUID: () => 'xyz' },
pid: 42,
now: () => 999,
});
// 末尾应该是 ".tmp.42.999.xyz"
expect(r.endsWith('.tmp.42.999.xyz')).toBe(true);
});
it('dst 含扩展名 / 含路径分隔符都正确保留', () => {
const r = generateTmpPath('C:/data/sub/note.md', {
crypto: { randomUUID: () => 'u' },
pid: 1,
now: () => 1,
});
expect(r).toBe('C:/data/sub/note.md.tmp.1.1.u');
});
it('默认(不传 crypto)走 node:crypto.randomUUID,real call 不抛', () => {
// 不传 opts.crypto → 走 require('node:crypto').randomUUID
const r = generateTmpPath('/data/note.md', { pid: 1, now: () => 1 });
// real randomUUID 返 36 字符的 UUID(8-4-4-4-12)
expect(r).toMatch(/\.tmp\.1\.1\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
});
it('默认 pid 走 process.pid,now 走 Date.now()', () => {
const r = generateTmpPath('/data/note.md');
// 路径第 3 段 = process.pid
const match = r.match(/\.tmp\.(\d+)\.(\d+)\./);
expect(match).toBeTruthy();
expect(Number(match[1])).toBe(process.pid);
// ts 应当接近 Date.now()(误差 < 5s)
const ts = Number(match[2]);
expect(Math.abs(ts - Date.now())).toBeLessThan(5000);
});
});