gongxiangp2/lib/__tests__/pdf-processor-limiter.test.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

163 lines
4.6 KiB
TypeScript

/** @jest-environment node */
const mockExecFile = jest.fn()
const mockReadFile = jest.fn()
const mockUnlink = jest.fn()
const mockSharp = jest.fn()
const mockEnsureEnoughDiskSpace = jest.fn()
jest.mock('child_process', () => ({
...jest.requireActual('child_process'),
execFile: mockExecFile,
}))
jest.mock('fs/promises', () => ({
readFile: mockReadFile,
unlink: mockUnlink,
writeFile: jest.fn(),
open: jest.fn(),
}))
jest.mock('sharp', () => ({
__esModule: true,
default: mockSharp,
}))
jest.mock('@/lib/sharp-runtime', () => ({
configureSharpRuntime: jest.fn(),
}))
jest.mock('@/lib/disk-space', () => ({
ensureEnoughDiskSpace: mockEnsureEnoughDiskSpace,
}))
async function waitUntil(condition: () => boolean): Promise<void> {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (condition()) {
return
}
await new Promise<void>((resolve) => setImmediate(resolve))
}
throw new Error('等待并发测试状态超时')
}
describe('generatePdfPreview image-processing limiter', () => {
afterEach(() => {
delete process.env.IMAGE_PROCESSING_MAX_CONCURRENT
})
test('两个 PDF 预览的 pdftoppm 与 Sharp 完整重处理段最大并发为 1', async () => {
process.env.IMAGE_PROCESSING_MAX_CONCURRENT = '1'
jest.resetModules()
jest.clearAllMocks()
const events: string[] = []
const execReleases: Array<() => void> = []
const sharpWriteReleases: Array<() => void> = []
let activeHeavyJobs = 0
let peakHeavyJobs = 0
let sourceImageCount = 0
let finalImageCount = 0
mockEnsureEnoughDiskSpace.mockResolvedValue(undefined)
mockReadFile.mockResolvedValue(Buffer.from('pdf-preview-image'))
mockUnlink.mockResolvedValue(undefined)
mockExecFile.mockImplementation((...args: unknown[]) => {
const callback = args[args.length - 1] as (
error: Error | null,
result: { stdout: string; stderr: string }
) => void
const jobNumber = execReleases.length + 1
activeHeavyJobs += 1
peakHeavyJobs = Math.max(peakHeavyJobs, activeHeavyJobs)
events.push(`pdftoppm:start:${jobNumber}`)
execReleases.push(() => callback(null, { stdout: '', stderr: '' }))
return {} as never
})
mockSharp.mockImplementation((input: Buffer | string) => {
if (Buffer.isBuffer(input)) {
const jobIndex = sourceImageCount
sourceImageCount += 1
const instance: any = {}
instance.metadata = jest.fn().mockResolvedValue({ width: 800, height: 600 })
instance.extract = jest.fn().mockReturnValue(instance)
instance.resize = jest.fn().mockReturnValue(instance)
instance.jpeg = jest.fn().mockReturnValue(instance)
instance.toFile = jest.fn(() => {
events.push(`sharp:write:${jobIndex + 1}`)
return new Promise<void>((resolve) => {
sharpWriteReleases[jobIndex] = resolve
})
})
return instance
}
const jobNumber = finalImageCount + 1
finalImageCount += 1
return {
metadata: jest.fn().mockImplementation(async () => {
events.push(`sharp:complete:${jobNumber}`)
activeHeavyJobs -= 1
return { width: 800, height: 600 }
}),
}
})
const { generatePdfPreview } = await import('@/lib/pdf-processor')
const firstPreview = generatePdfPreview('first.pdf', 'first-preview.jpg')
const secondPreview = generatePdfPreview('second.pdf', 'second-preview.jpg')
await waitUntil(() => execReleases.length === 1)
expect(mockExecFile).toHaveBeenCalledTimes(1)
expect(activeHeavyJobs).toBe(1)
execReleases[0]()
await waitUntil(() => sharpWriteReleases.length === 1)
// 第一个 pdftoppm 已结束,但 Sharp 仍在写盘;第二个 PDF 仍必须排队。
expect(mockExecFile).toHaveBeenCalledTimes(1)
expect(activeHeavyJobs).toBe(1)
sharpWriteReleases[0]()
await expect(firstPreview).resolves.toEqual({
success: true,
previewPath: 'first-preview.jpg',
width: 800,
height: 600,
})
await waitUntil(() => execReleases.length === 2)
expect(activeHeavyJobs).toBe(1)
execReleases[1]()
await waitUntil(() => sharpWriteReleases.length === 2)
sharpWriteReleases[1]()
await expect(secondPreview).resolves.toEqual({
success: true,
previewPath: 'second-preview.jpg',
width: 800,
height: 600,
})
expect(peakHeavyJobs).toBe(1)
expect(activeHeavyJobs).toBe(0)
expect(events).toEqual([
'pdftoppm:start:1',
'sharp:write:1',
'sharp:complete:1',
'pdftoppm:start:2',
'sharp:write:2',
'sharp:complete:2',
])
})
})