'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); const { execFile } = require('child_process'); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-worker-lifecycle-')); const library = path.join(root, 'test.library'); const source = path.join(library, 'images', 'test.info', 'hero.atlas.txt'); const marker = path.join(root, 'completed'); const worker = path.resolve(__dirname, '../js/spine-reconcile-worker.js'); const thumbnail = path.resolve(__dirname, '../thumbnail/spine.js'); const key = crypto.createHash('sha256').update(path.dirname(worker) + library).digest('hex'); const queue = path.join(os.tmpdir(), 'spineviewer-reconcile', key); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); (async () => { try { fs.mkdirSync(path.dirname(source), { recursive: true }); fs.writeFileSync(source, 'atlas'); fs.writeFileSync(path.join(library, 'metadata.json'), '{"folders":[]}'); // Isolate lifecycle behavior from rendering and the user's running Eagle API. const preload = path.join(root, 'preload.cjs'); fs.writeFileSync(preload, `require.cache[${JSON.stringify(thumbnail)}] = { exports: { reconcileOnce: async (src, previous) => { if (previous !== 'stable') return { signature: 'stable', complete: false }; require('fs').appendFileSync(${JSON.stringify(marker)}, String(process.pid) + '\\n'); return { complete: true }; } }};`); await new Promise((resolve, reject) => execFile(process.execPath, ['-e', `require(${JSON.stringify(worker)}).enqueue(${JSON.stringify(source)})`], { env: { ...process.env, NODE_OPTIONS: '--require=' + preload } }, error => error ? reject(error) : resolve())); assert(!fs.existsSync(marker), 'launcher returns before reconciliation'); const deadline = Date.now() + 15000; while ((!fs.existsSync(marker) || fs.existsSync(path.join(queue, 'worker.lock'))) && Date.now() < deadline) await sleep(100); assert(fs.existsSync(marker), 'worker completes after launching process has exited'); assert(!fs.existsSync(path.join(queue, 'worker.lock')), 'worker releases lock and exits when idle'); assert.equal(fs.readdirSync(queue).filter(name => name.endsWith('.json')).length, 0); console.log('PASS: detached worker survives callback process exit, completes queued work and releases lock'); } finally { if (!fs.existsSync(path.join(queue, 'worker.lock'))) { fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(queue, { recursive: true, force: true }); } } })().catch(error => { console.error(error); process.exitCode = 1; });