// Phase A cancel verification: abort renderMp4Job mid-export, confirm it // rejects, leaves no partial output file, and the ffmpeg child is gone. // // Run: npx tsc -p tsconfig.electron.json && node_modules/.bin/electron scripts/verify-cancel.cjs const { app } = require('electron') const path = require('node:path') const os = require('node:os') const fs = require('node:fs') const { renderMp4Job } = require('../dist-electron/renderWorker.cjs') const fps = 24 const duration = 6 // long enough that we can cancel before it finishes const scene = { canvas: { width: 480, height: 360, background: '#202030' }, duration, assets: [{ id: 'a1', kind: 'rect', width: 60, height: 60, fill: '#40c0ff' }], layers: [ { id: 'l1', assetId: 'a1', visible: true, base: { x: 40, y: 180, scaleX: 1, scaleY: 1, rotation: 0, opacity: 1 }, keyframes: [ { time: 0, props: { x: 40 } }, { time: 6, props: { x: 420 } }, ], }, ], } const options = { fps, duration, totalFrames: fps * duration + 1, crf: '20', preset: 'ultrafast' } async function main() { const outPath = path.join(os.tmpdir(), 'ruwio-verify-cancel.mp4') fs.rmSync(outPath, { force: true }) const controller = new AbortController() let progressAtCancel = null const job = renderMp4Job({ jobId: 'cancel', scene, options, outputPath: outPath, signal: controller.signal, onProgress: (p) => { // Cancel once we've made real progress but are nowhere near done. if (progressAtCancel === null && p > 0.1) { progressAtCancel = p controller.abort() } }, }) let rejected = false let message = '' try { await job } catch (err) { rejected = true message = err.message } // Give the OS a moment to reap the killed ffmpeg + finish fs.rm cleanup. await new Promise((r) => setTimeout(r, 300)) const notes = [] let pass = true if (progressAtCancel === null) { pass = false notes.push('never reached a progress point to cancel at (job too fast)') } if (!rejected) { pass = false notes.push('job resolved instead of rejecting after abort') } else if (!/abort/i.test(message)) { notes.push(`rejected, but message was: ${message}`) } if (fs.existsSync(outPath)) { pass = false notes.push(`partial output left behind: ${outPath} (${fs.statSync(outPath).size}B)`) } console.log(`progressAtCancel=${progressAtCancel} rejected=${rejected} message="${message}"`) console.log(`output exists after cancel: ${fs.existsSync(outPath)}`) console.log(notes.length ? 'NOTES:\n ' + notes.join('\n ') : 'NOTES: none') console.log(pass ? 'RESULT: PASS' : 'RESULT: FAIL') app.exit(pass ? 0 : 1) } // Suppress Electron's default "quit when all windows close": renderMp4Job // closes its render window, and the auto-quit would race our result printing. app.on('window-all-closed', () => {}) app.whenReady().then(() => main().catch((err) => { console.error('HARNESS ERROR:', err) app.exit(2) }), )