/** * R38修复:磁盘空间检查测试 * * 测试内容: * - formatBytes 函数 * - Node statfs 跨平台磁盘空间读取 * - getDiskUsageLevel / hasEnoughDiskSpace / ensureEnoughDiskSpace 逻辑 */ import { statfs } from 'fs/promises' import { checkDiskSpace, ensureEnoughDiskSpace, formatBytes, getDiskUsageLevel, hasEnoughDiskSpace, } from '../disk-space' jest.mock('fs/promises', () => { const originalModule = jest.requireActual('fs/promises') return { ...originalModule, statfs: jest.fn(), } }) const mockedStatfs = statfs as jest.MockedFunction const GIBIBYTE = BigInt(1024) * BigInt(1024) * BigInt(1024) function mockSpace(total: bigint, free: bigint, available = free, blockSize = BigInt(1)) { mockedStatfs.mockResolvedValueOnce({ type: BigInt(0), bsize: blockSize, blocks: total / blockSize, bfree: free / blockSize, bavail: available / blockSize, files: BigInt(0), ffree: BigInt(0), }) } describe('R38: disk-space', () => { beforeEach(() => { jest.clearAllMocks() }) describe('checkDiskSpace', () => { it('使用 Node statfs 读取字节数,不依赖系统 df 命令', async () => { mockSpace(BigInt(100_000), BigInt(80_000), BigInt(75_000)) await expect(checkDiskSpace('/app/uploads/transcoded')).resolves.toEqual({ total: 100_000, used: 20_000, available: 75_000, usePercent: 22, mountPoint: '/app/uploads/transcoded', }) expect(mockedStatfs).toHaveBeenCalledWith('/app/uploads/transcoded', { bigint: true }) }) it('按块大小和非特权可用块计算并向上取整使用率', async () => { mockSpace(BigInt(12_288), BigInt(8_192), BigInt(4_096), BigInt(4_096)) const result = await checkDiskSpace('/app/uploads') expect(result).toMatchObject({ total: 12_288, used: 4_096, available: 4_096, usePercent: 50, }) }) it('不把文件系统保留块算作 nextjs 可用空间', async () => { mockSpace(BigInt(100), BigInt(20), BigInt(10)) await expect(checkDiskSpace('/app/uploads')).resolves.toMatchObject({ total: 100, used: 80, available: 10, usePercent: 89, }) }) it('超出安全整数范围时钳制数值,避免精度溢出绕过空间门禁', async () => { const huge = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(10_000) mockSpace(huge, huge, huge) const result = await checkDiskSpace('/app/uploads') expect(result?.total).toBe(Number.MAX_SAFE_INTEGER) expect(result?.available).toBe(Number.MAX_SAFE_INTEGER) expect(result?.used).toBe(0) }) it('空文件系统统计不会产生除零或非有限使用率', async () => { mockSpace(BigInt(0), BigInt(0), BigInt(0)) await expect(checkDiskSpace('/app/uploads')).resolves.toMatchObject({ total: 0, used: 0, available: 0, usePercent: 0, }) }) it('statfs 失败时返回 null,让严格调用方保持 fail-closed', async () => { mockedStatfs.mockRejectedValueOnce(Object.assign(new Error('stale mount'), { code: 'ESTALE' })) await expect(checkDiskSpace('/app/uploads')).resolves.toBeNull() }) }) describe('formatBytes', () => { it('应该正确格式化字节数', () => { expect(formatBytes(0)).toBe('0.00 B') expect(formatBytes(1024)).toBe('1.00 KB') expect(formatBytes(1024 * 1024)).toBe('1.00 MB') expect(formatBytes(1024 * 1024 * 1024)).toBe('1.00 GB') expect(formatBytes(1024 * 1024 * 1024 * 1024)).toBe('1.00 TB') expect(formatBytes(1536)).toBe('1.50 KB') }) }) describe('getDiskUsageLevel - 逻辑验证', () => { it('使用率低于 70% 应该返回 ok', async () => { mockSpace(BigInt(100_000), BigInt(40_000)) const result = await getDiskUsageLevel() expect(result).toBe('ok') }) it('使用率在 70%-90% 之间应该返回 warning', async () => { mockSpace(BigInt(100_000), BigInt(25_000)) const result = await getDiskUsageLevel() expect(result).toBe('warning') }) it('使用率大于 90% 应该返回 critical', async () => { mockSpace(BigInt(100_000), BigInt(5_000)) const result = await getDiskUsageLevel() expect(result).toBe('critical') }) it('无法检查时应该返回 ok', async () => { mockedStatfs.mockRejectedValueOnce(new Error('statfs failed')) const result = await getDiskUsageLevel() expect(result).toBe('ok') }) }) describe('hasEnoughDiskSpace - 逻辑验证', () => { it('当空间足够时应该返回 true', async () => { mockSpace(BigInt(100) * GIBIBYTE, BigInt(80) * GIBIBYTE) const result = await hasEnoughDiskSpace(10 * 1024 * 1024 * 1024) expect(result).toBe(true) }) it('当可用空间不足时应该返回 false', async () => { mockSpace(BigInt(100) * GIBIBYTE, BigInt(10) * GIBIBYTE) const result = await hasEnoughDiskSpace(20 * 1024 * 1024 * 1024) expect(result).toBe(false) }) it('当磁盘使用率过高时应该返回 false', async () => { mockSpace(BigInt(100) * GIBIBYTE, BigInt(5) * GIBIBYTE) const result = await hasEnoughDiskSpace(100 * 1024 * 1024) expect(result).toBe(false) }) it('当无法检查时应该返回 true(保守处理)', async () => { mockedStatfs.mockRejectedValueOnce(new Error('statfs failed')) const result = await hasEnoughDiskSpace(10 * 1024 * 1024 * 1024) expect(result).toBe(true) }) }) describe('ensureEnoughDiskSpace - 逻辑验证', () => { it('当空间足够时不应该抛出错误', async () => { mockSpace(BigInt(100) * GIBIBYTE, BigInt(80) * GIBIBYTE) await expect(ensureEnoughDiskSpace(10 * 1024 * 1024 * 1024, '测试操作')).resolves.not.toThrow() }) it('当空间不足时应该抛出错误', async () => { mockSpace(BigInt(100) * GIBIBYTE, BigInt(10) * GIBIBYTE) await expect(ensureEnoughDiskSpace(20 * 1024 * 1024 * 1024, '测试操作')).rejects.toThrow('磁盘空间不足') }) it('当无法检查时不应该抛出错误', async () => { mockedStatfs.mockRejectedValueOnce(new Error('statfs failed')) await expect(ensureEnoughDiskSpace(10 * 1024 * 1024 * 1024, '测试操作')).resolves.not.toThrow() }) }) describe('数值计算验证', () => { it('应该正确计算使用率百分比', () => { const total = 100000000000 const used = 70000000000 const usePercent = (used / total) * 100 expect(usePercent).toBe(70) }) it('应该正确计算保留空间', () => { const total = 100000000000 const usePercent = 90 const minReserved = Math.max(total * 0.1, 5 * 1024 * 1024 * 1024) expect(minReserved).toBeGreaterThanOrEqual(5 * 1024 * 1024 * 1024) // 至少 5GB }) it('应该正确格式化字节大小', () => { const bytes = 10 * 1024 * 1024 * 1024 expect(formatBytes(bytes)).toBe('10.00 GB') }) }) })