update
This commit is contained in:
129
tests/unit/main/utils/file-check.test.ts
Normal file
129
tests/unit/main/utils/file-check.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
// tests/unit/main/utils/file-check.test.ts
|
||||
//
|
||||
// 守 assertFileExists 的**错误分类**契约(2026-10-20,r201):
|
||||
//
|
||||
// fs.open('r') 失败时的错误码必须区分:
|
||||
// ENOENT / 无 code → FILE_MISSING("文件不存在")
|
||||
// 其它( EACCES / EPERM / EISDIR / EBUSY …)→ IO_ERROR
|
||||
//
|
||||
// 为什么这条分类是行为面(不是风格):调用方(notes 的"在文件夹中显示"、文献
|
||||
// 的 open 路径)据此决定 UI 文案与是否可重试。旧实现一律 FILE_MISSING,
|
||||
// 权限问题被错报成"文件不存在",用户不停 retry,真实根因被埋没 —— r33 修过
|
||||
// 一次(r33 只分了 ENOENT / 其它两支),r96 又把"拿不到 code"这一支从
|
||||
// IO_ERROR 改回 FILE_MISSING(保守:不认识 → 当成不存在)。
|
||||
//
|
||||
// 本文件锁住 r96 之后的三支语义 —— 源码注释里明确写着"摘 !code 分支必红":
|
||||
// 1. ENOENT → FILE_MISSING
|
||||
// 2. 错误对象**没有** code → FILE_MISSING(保守兜底)
|
||||
// 3. EACCES 等其它 code → IO_ERROR
|
||||
// 4. 空路径 → FILE_MISSING
|
||||
// 5. 打开成功 → 句柄一定被 close(不泄漏 fd)
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { mkdtemp, rm, writeFile } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { assertFileExists } from '../../../../src/main/utils/file-check';
|
||||
import { createLogger } from '../../../../src/main/utils/logger';
|
||||
|
||||
const log = createLogger('file-check-test');
|
||||
|
||||
/** 造一个带 errno code 的错误(模拟 EACCES / EPERM / EISDIR / EBUSY) */
|
||||
function fsError(code: string, msg: string): NodeJS.ErrnoException {
|
||||
const e = new Error(msg) as NodeJS.ErrnoException;
|
||||
e.code = code;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** 取 LibraryError 的 code */
|
||||
function codeOf(e: unknown): string | undefined {
|
||||
return (e as { code?: string }).code;
|
||||
}
|
||||
|
||||
describe('assertFileExists — 错误分类(r201)', () => {
|
||||
let tmpRoot: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpRoot = await mkdtemp(join(tmpdir(), 'rkms-filecheck-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await rm(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('空路径 → FILE_MISSING(不发起 syscall)', async () => {
|
||||
const spy = vi.spyOn(fsPromises, 'open');
|
||||
const err = await assertFileExists('', '文件不存在', log).then(
|
||||
() => null,
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(codeOf(err)).toBe('FILE_MISSING');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('文件存在 → 正常 resolve', async () => {
|
||||
const p = join(tmpRoot, 'there.md');
|
||||
await writeFile(p, '# hi', 'utf-8');
|
||||
await expect(assertFileExists(p, '文件不存在', log)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('文件不存在(ENOENT)→ FILE_MISSING,且消息带上 errno 文本便于排查', async () => {
|
||||
const p = join(tmpRoot, 'missing.md');
|
||||
const err = await assertFileExists(p, '文件不存在', log).then(
|
||||
() => null,
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(codeOf(err)).toBe('FILE_MISSING');
|
||||
expect((err as Error).message).toContain('文件不存在');
|
||||
expect((err as Error).message).toMatch(/ENOENT|no such file/i);
|
||||
});
|
||||
|
||||
it('无 code 的错误 → FILE_MISSING(r96 保守兜底:不认识就当不存在)', async () => {
|
||||
const spy = vi
|
||||
.spyOn(fsPromises, 'open')
|
||||
.mockImplementationOnce(() => Promise.reject(new Error('weird failure')) as never);
|
||||
const err = await assertFileExists(join(tmpRoot, 'x.md'), '文件不存在', log).then(
|
||||
() => null,
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(codeOf(err)).toBe('FILE_MISSING');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('EACCES → IO_ERROR(不是 FILE_MISSING)', async () => {
|
||||
vi.spyOn(fsPromises, 'open').mockImplementationOnce(
|
||||
() => Promise.reject(fsError('EACCES', 'permission denied')) as never,
|
||||
);
|
||||
const err = await assertFileExists(join(tmpRoot, 'x.md'), '文件不存在', log).then(
|
||||
() => null,
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(codeOf(err)).toBe('IO_ERROR');
|
||||
});
|
||||
|
||||
it('EISDIR(路径其实是目录)→ IO_ERROR', async () => {
|
||||
vi.spyOn(fsPromises, 'open').mockImplementationOnce(
|
||||
() => Promise.reject(fsError('EISDIR', 'illegal operation on a directory')) as never,
|
||||
);
|
||||
const err = await assertFileExists(join(tmpRoot, 'dir.md'), '文件不存在', log).then(
|
||||
() => null,
|
||||
(e: unknown) => e,
|
||||
);
|
||||
expect(codeOf(err)).toBe('IO_ERROR');
|
||||
});
|
||||
|
||||
it('打开成功 → 句柄一定被 close(不泄漏 fd)', async () => {
|
||||
const close = vi.fn(async () => undefined);
|
||||
const fakeFh = { close } as unknown as Awaited<ReturnType<typeof fsPromises.open>>;
|
||||
vi.spyOn(fsPromises, 'open').mockImplementationOnce((() =>
|
||||
Promise.resolve(fakeFh)) as unknown as typeof fsPromises.open);
|
||||
await expect(
|
||||
assertFileExists(join(tmpRoot, 'ok.md'), '文件不存在', log),
|
||||
).resolves.toBeUndefined();
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user