Files
hermes-ui/tests/server/safe-file-store-backup-fallback.test.ts
T
toor a1fff482bf make the test suite and openapi generator platform-independent
Windows checkouts turned LF into CRLF (core.autocrlf=true), and several
tests plus the openapi generator hardcoded LF or POSIX path separators.
The result was a suite that was green on Linux CI but red on Windows, and
an openapi.json whose /api/hermes/* routes silently disappeared when
generated on Windows.

Pin the repository to LF via .gitattributes, strip shebangs before
Vite's SSR transform, and key route files by their posix path. Add a
manually triggered macOS verification workflow so the same checks run
on a self-hosted macOS runner.
2026-10-20 11:17:34 +08:00

71 lines
2.5 KiB
TypeScript
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.
import { afterEach, describe, expect, it, vi } from 'vitest'
import { resolve } from 'path'
const {
mockCopyFile,
mockMkdir,
mockReadFile,
mockRename,
mockRm,
mockWriteFile,
} = vi.hoisted(() => ({
mockCopyFile: vi.fn(),
mockMkdir: vi.fn(),
mockReadFile: vi.fn(),
mockRename: vi.fn(),
mockRm: vi.fn(),
mockWriteFile: vi.fn(),
}))
vi.mock('fs/promises', () => ({
copyFile: mockCopyFile,
mkdir: mockMkdir,
readFile: mockReadFile,
rename: mockRename,
rm: mockRm,
writeFile: mockWriteFile,
}))
afterEach(() => {
vi.clearAllMocks()
})
describe('SafeFileStore backup fallback', () => {
it('uses a timestamped backup when the default backup cannot be overwritten', async () => {
mockCopyFile
.mockRejectedValueOnce(Object.assign(new Error('permission denied'), { code: 'EACCES' }))
.mockResolvedValueOnce(undefined)
mockMkdir.mockResolvedValue(undefined)
mockWriteFile.mockResolvedValue(undefined)
mockRename.mockResolvedValue(undefined)
const { SafeFileStore } = await import('../../packages/server/src/services/safe-file-store')
const store = new SafeFileStore()
// SafeFileStore.normalizePath() 走 resolve(),Windows 上 resolve('/tmp/config.yaml')
// = 'D:\tmp\config.yaml'。断言必须按生产语义写死,否则只在 Windows 上红。
const target = resolve('/tmp/config.yaml')
await store.writeText('/tmp/config.yaml', 'model:\n default: new\n', { backup: true })
expect(mockCopyFile).toHaveBeenCalledTimes(2)
expect(mockCopyFile).toHaveBeenNthCalledWith(1, target, `${target}.bak`)
expect(mockCopyFile.mock.calls[1][0]).toBe(target)
expect(mockCopyFile.mock.calls[1][1].startsWith(`${target}.bak.`)).toBe(true)
expect(mockWriteFile).toHaveBeenCalledWith(expect.stringContaining(`${target}.tmp.`), 'model:\n default: new\n', 'utf-8')
expect(mockRename).toHaveBeenCalledWith(expect.stringContaining(`${target}.tmp.`), target)
})
it('preserves explicit backup path failures', async () => {
mockCopyFile.mockRejectedValueOnce(Object.assign(new Error('permission denied'), { code: 'EACCES' }))
mockMkdir.mockResolvedValue(undefined)
const { SafeFileStore } = await import('../../packages/server/src/services/safe-file-store')
const store = new SafeFileStore()
await expect(store.writeText('/tmp/config.yaml', 'new', { backup: true, backupPath: '/tmp/custom.bak' })).rejects.toThrow('permission denied')
expect(mockCopyFile).toHaveBeenCalledTimes(1)
expect(mockWriteFile).not.toHaveBeenCalled()
})
})