// Smoke-test harness for the Windows VST sandbox path. Loads the addon, // spawns the sandbox subprocess for Guitar Rig 6, opens its editor, then // closes and shuts down cleanly. Used by clean-rerun.cmd on the test VM. // // Run from the repo root: `node scripts/dev/load-gr6.js > load-gr6-sandbox.log`. // The Guitar Rig 6 path is hardcoded to its standard Win11 install location; // adjust the GR6 path below if your install differs. 'use strict'; const path = require('path'); const addonPath = path.join(process.cwd(), 'build', 'Release', 'slopsmith_audio.node'); console.log('[test] loading addon from', addonPath); const addon = require(addonPath); console.log('[test] addon loaded; methods:', Object.keys(addon).slice(0, 10).join(', '), '...'); // Global watchdog — best-effort coverage for *asynchronous* hang paths // (event-loop livelocks, setTimeout-stacked cleanup). loadVST is now a // Napi::AsyncWorker, so the libuv event loop continues to fire timers // while the load runs on a worker thread; this watchdog can pre-empt // a hung async load. addon.shutdown still parks the event loop // synchronously inside dispatchOnMessageThread, so if shutdown itself // hangs the timer callback never fires — a proper supervisor-process // + SIGKILL watchdog belongs in the CI harness (test-suite follow-up). // // The timer callback hard-exits — do NOT call addon.shutdown() here, // it would block on the same dispatchOnMessageThread the addon is // already stuck in and deadlock the process. const WATCHDOG_MS = 60000; const watchdog = setTimeout(() => { console.error(`[test] FATAL: watchdog tripped after ${WATCHDOG_MS} ms (async hang)`); process.exit(1); }, WATCHDOG_MS); function failExit(msg) { if (msg) console.log('[test] FAIL:', msg); // addon.shutdown blocks on dispatchOnMessageThread (up to 15s) if // JUCE init partially succeeded; an init-failure path that triggered // *because* the message thread never came up would then time out // before the process exits. Cap with a hard process.exit timer so a // hung shutdown can't extend the failure window beyond 3s. const hardKill = setTimeout(() => { console.error('[test] FAIL: addon.shutdown hung, force-exiting'); process.exit(2); }, 3000); hardKill.unref(); try { addon.shutdown(); } catch (_) {} try { clearTimeout(watchdog); } catch (_) {} try { clearTimeout(hardKill); } catch (_) {} process.exit(1); } console.log('[test] addon.init()'); try { addon.init(); } catch (e) { failExit('EXCEPTION on init: ' + e.message); } setTimeout(async () => { // Allow override for CI / dev machines whose VST3 layout differs from // the standard "C:\Program Files\Common Files\VST3" install location. // The default Native Instruments install ships "Guitar Rig 6.vst3"; // some installer versions or FX-only variants land as // "Guitar Rig 6 FX.vst3". Try both before giving up. const fs = require('fs'); const candidates = process.env.GR6_PATH ? [process.env.GR6_PATH] : [ // NI's own installer drops into a vendor subdir; this is the // most common default on a fresh GR6 install. 'C:\\Program Files\\Native Instruments\\VST3\\Guitar Rig 6.vst3', 'C:\\Program Files\\Native Instruments\\VST3\\Guitar Rig 6 FX.vst3', // Some installs (and the existing CI fixture VM) drop into the // shared Common Files VST3 dir; keep these as fallbacks so the // existing smoke harness doesn't have to flip overnight. 'C:\\Program Files\\Common Files\\VST3\\Guitar Rig 6.vst3', 'C:\\Program Files\\Common Files\\VST3\\Guitar Rig 6 FX.vst3', ]; const gr6 = candidates.find(p => { try { return fs.existsSync(p); } catch (_) { return false; } }); if (!gr6) { console.error('[test] FATAL: no Guitar Rig 6 install found at any of:'); for (const p of candidates) console.error(' - ' + p); console.error('Set GR6_PATH to override (e.g. GR6_PATH="C:\\path\\to\\Guitar Rig 6.vst3" node scripts\\dev\\load-gr6.js).'); try { addon.shutdown(); } catch (_) {} clearTimeout(watchdog); process.exit(2); return; } console.log('[test] calling addon.loadVST(' + gr6 + ')'); let slot; try { // addon.loadVST is now a Promise (Napi::AsyncWorker); await // it. The enclosing setTimeout callback was made async above. slot = await addon.loadVST(gr6); console.log('[test] loadVST returned slot:', slot); } catch (e) { failExit('EXCEPTION on loadVST: ' + e.message); return; } if (!Number.isInteger(slot) || slot < 0) { failExit('loadVST returned invalid slot: ' + String(slot)); return; } setTimeout(() => { console.log('[test] calling addon.openPluginEditor(' + slot + ')'); let ok = false; try { ok = addon.openPluginEditor(slot); console.log('[test] openPluginEditor returned:', ok); } catch (e) { failExit('EXCEPTION on openPluginEditor: ' + e.message); return; } if (!ok) { try { addon.closePluginEditor(slot); } catch (_) {} failExit('openPluginEditor returned false'); return; } console.log('[test] sleeping 5s for editor creation + potential crash...'); setTimeout(() => { console.log('[test] still alive after editor wait; closing'); try { addon.closePluginEditor(slot); } catch (e) {} try { addon.shutdown(); } catch (e) {} clearTimeout(watchdog); setTimeout(() => process.exit(0), 1000); }, 5000); }, 1500); }, 2000);