// Phase B verification of the inactivity watchdog (finding #3): point the // worker at a render page whose __animatrSeek never resolves, with a short // RUWIO_RENDER_TIMEOUT_MS, and confirm the job aborts with "render timed out" // and leaves no partial file. // // Run: npx tsc -p tsconfig.electron.json && node_modules/.bin/electron scripts/verify-timeout.cjs // Must be set BEFORE requiring the worker — the timeout is read at module load. process.env.RUWIO_RENDER_TIMEOUT_MS = '2000' const { app } = require('electron') const path = require('node:path') const os = require('node:os') const fs = require('node:fs') // Build a temp project dir with a server/render.html that hangs on seek, then // chdir into it so the worker's loadRenderPage (process.cwd()/server/render.html) // picks it up. const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruwio-timeout-')) fs.mkdirSync(path.join(tmpDir, 'server')) fs.writeFileSync( path.join(tmpDir, 'server', 'render.html'), ``, ) process.chdir(tmpDir) const { renderMp4Job } = require('../dist-electron/renderWorker.cjs') const fps = 12 const duration = 2 const scene = { canvas: { width: 160, height: 120, background: '#202030' }, duration, assets: [{ id: 'a1', kind: 'rect', width: 20, height: 20, fill: '#ff4040' }], layers: [ { id: 'l1', assetId: 'a1', visible: true, base: { x: 20, y: 60, scaleX: 1, scaleY: 1, rotation: 0, opacity: 1 }, keyframes: [{ time: 0, props: { x: 20 } }, { time: 2, props: { x: 140 } }], }, ], } const options = { fps, duration, totalFrames: fps * duration + 1, crf: '20', preset: 'ultrafast' } app.on('window-all-closed', () => {}) app.whenReady().then(async () => { const outPath = path.join(tmpDir, 'out.mp4') const controller = new AbortController() const startedAt = process.hrtime.bigint() let rejected = false let message = '' try { await renderMp4Job({ jobId: 'timeout', scene, options, outputPath: outPath, signal: controller.signal, onProgress: () => {}, }) } catch (err) { rejected = true message = err.message } const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6 await new Promise((r) => setTimeout(r, 200)) const notes = [] let pass = true if (!rejected) { pass = false; notes.push('job did not reject') } if (!/timed out/i.test(message)) { pass = false; notes.push(`message was "${message}", expected "render timed out"`) } if (elapsedMs < 1500 || elapsedMs > 8000) { pass = false; notes.push(`elapsed ${elapsedMs.toFixed(0)}ms outside [1500,8000] (timeout=2000)`) } if (fs.existsSync(outPath)) { pass = false; notes.push('partial output left behind') } console.log(`rejected=${rejected} message="${message}" elapsed=${elapsedMs.toFixed(0)}ms outExists=${fs.existsSync(outPath)}`) console.log(notes.length ? 'NOTES:\n ' + notes.join('\n ') : 'NOTES: none') console.log(pass ? 'RESULT: PASS' : 'RESULT: FAIL') fs.rmSync(tmpDir, { recursive: true, force: true }) app.exit(pass ? 0 : 1) })