import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { getPool, closePool } from '../src/db.js'; import { loadConfig } from '../src/config.js'; import { ensureStorageDirs, uploadPath, outputPath } from '../src/storage.js'; import { createJob, markDone, getJobByUuid } from '../src/jobs/jobRepository.js'; import { runCleanup } from '../src/cleanup.js'; let pool; let config; beforeAll(async () => { config = { ...loadConfig(), storageDir: await fs.mkdtemp(path.join(os.tmpdir(), 'converter-cleanup-')) }; await ensureStorageDirs(config); pool = getPool(config); }); afterAll(async () => { await closePool(); await fs.rm(config.storageDir, { recursive: true, force: true }); }); beforeEach(async () => { await pool.query('DELETE FROM conversion_jobs'); }); describe('runCleanup', () => { it('marks an expired done job cleaned and removes its input and output files, keeping the row', async () => { const uuid = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'; await fs.writeFile(uploadPath(config, uuid, 'png'), 'input bytes'); await fs.writeFile(outputPath(config, uuid, 'webp'), 'output bytes'); await createJob(pool, { uuid, family: 'image', sourceFormat: 'png', targetFormat: 'webp', originalFilename: 'photo.png', inputPath: `${uuid}.png`, inputMimeType: 'image/png', inputSizeBytes: 11, expiresAt: new Date(Date.now() - 1000), }); const created = await getJobByUuid(pool, uuid); await markDone(pool, created.id, { outputPath: `${uuid}.webp`, outputMimeType: 'image/webp', outputSizeBytes: 12, conversionDurationSeconds: 0.5, }); const cleanedCount = await runCleanup(pool, config); expect(cleanedCount).toBe(1); const job = await getJobByUuid(pool, uuid); expect(job).not.toBeNull(); expect(job.cleanedAt).not.toBeNull(); await expect(fs.stat(uploadPath(config, uuid, 'png'))).rejects.toThrow(); await expect(fs.stat(outputPath(config, uuid, 'webp'))).rejects.toThrow(); }); it('marks an expired pending job (with no output file) cleaned without throwing', async () => { const uuid = 'ffffffff-ffff-4fff-8fff-ffffffffffff'; await fs.writeFile(uploadPath(config, uuid, 'png'), 'input bytes'); await createJob(pool, { uuid, family: 'image', sourceFormat: 'png', targetFormat: 'webp', originalFilename: 'photo.png', inputPath: `${uuid}.png`, inputMimeType: 'image/png', inputSizeBytes: 11, expiresAt: new Date(Date.now() - 1000), }); const cleanedCount = await runCleanup(pool, config); expect(cleanedCount).toBe(1); const job = await getJobByUuid(pool, uuid); expect(job).not.toBeNull(); expect(job.cleanedAt).not.toBeNull(); }); it('leaves non-expired jobs untouched', async () => { const uuid = '12121212-1212-4212-8212-121212121212'; await createJob(pool, { uuid, family: 'image', sourceFormat: 'png', targetFormat: 'webp', originalFilename: 'photo.png', inputPath: `${uuid}.png`, inputMimeType: 'image/png', inputSizeBytes: 11, expiresAt: new Date(Date.now() + 3600 * 1000), }); const cleanedCount = await runCleanup(pool, config); expect(cleanedCount).toBe(0); const job = await getJobByUuid(pool, uuid); expect(job).not.toBeNull(); expect(job.cleanedAt).toBeNull(); }); it('does not re-process an already cleaned expired job', async () => { const uuid = '13131313-1313-4313-8313-131313131313'; await createJob(pool, { uuid, family: 'image', sourceFormat: 'png', targetFormat: 'webp', originalFilename: 'photo.png', inputPath: `${uuid}.png`, inputMimeType: 'image/png', inputSizeBytes: 11, expiresAt: new Date(Date.now() - 1000), }); const firstRun = await runCleanup(pool, config); const secondRun = await runCleanup(pool, config); expect(firstRun).toBe(1); expect(secondRun).toBe(0); }); });