import { test, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { DENOISE_MODELS, ML_DENOISE_REQUIREMENTS, isMLDenoiseSupported, } from './lotusDenoiseUtils'; // ── Model catalog (data integrity) ────────────────────────────────────────── test('DENOISE_MODELS lists the four expected models in order', () => { assert.deepEqual( DENOISE_MODELS.map((m) => m.id), ['rnnoise', 'speex', 'dtln', 'deepfilternet'], ); }); test('DENOISE_MODELS ids are unique', () => { const ids = DENOISE_MODELS.map((m) => m.id); assert.equal(new Set(ids).size, ids.length); }); test('every model has non-empty display fields and valid rating enums', () => { const transients = new Set(['Poor', 'Good', 'Excellent']); const voice = new Set(['Moderate', 'High', 'Very High']); for (const m of DENOISE_MODELS) { for (const field of ['name', 'description', 'cpuUsage', 'binarySize'] as const) { assert.ok(typeof m[field] === 'string' && m[field].length > 0, `${m.id}.${field} empty`); } assert.ok(transients.has(m.transients), `${m.id} bad transients: ${m.transients}`); assert.ok(voice.has(m.voiceQuality), `${m.id} bad voiceQuality: ${m.voiceQuality}`); } }); test('ML_DENOISE_REQUIREMENTS is a non-empty list of strings', () => { assert.ok(Array.isArray(ML_DENOISE_REQUIREMENTS) && ML_DENOISE_REQUIREMENTS.length > 0); assert.ok(ML_DENOISE_REQUIREMENTS.every((r) => typeof r === 'string' && r.length > 0)); }); // ── isMLDenoiseSupported (feature detection) ──────────────────────────────── const g = globalThis as Record; const NAMES = ['window', 'navigator', 'AudioWorkletNode'] as const; let saved: Record; const setGlobal = (name: string, value: unknown): void => { Object.defineProperty(g, name, { value, configurable: true, writable: true }); }; const removeGlobal = (name: string): void => { // Make a bare reference to `name` throw ReferenceError (simulate an absent // global binding), as it would in a browser lacking the API entirely. if (Object.getOwnPropertyDescriptor(g, name)) delete g[name]; }; const withMediaDevices = { mediaDevices: { getUserMedia: () => Promise.resolve() } }; beforeEach(() => { saved = {}; for (const n of NAMES) saved[n] = Object.getOwnPropertyDescriptor(g, n); }); afterEach(() => { for (const n of NAMES) { const d = saved[n]; if (d) Object.defineProperty(g, n, d); else if (Object.getOwnPropertyDescriptor(g, n)) delete g[n]; } }); test('returns false when there is no window (non-browser)', () => { setGlobal('window', undefined); assert.equal(isMLDenoiseSupported(), false); }); test('returns true when AudioContext, AudioWorklet and getUserMedia are present', () => { setGlobal('window', { AudioContext: function AudioContext() {} }); setGlobal('AudioWorkletNode', function AudioWorkletNode() {}); setGlobal('navigator', withMediaDevices); assert.equal(isMLDenoiseSupported(), true); }); test('accepts the legacy webkitAudioContext prefix', () => { setGlobal('window', { webkitAudioContext: function webkitAudioContext() {} }); setGlobal('AudioWorkletNode', function AudioWorkletNode() {}); setGlobal('navigator', withMediaDevices); assert.equal(isMLDenoiseSupported(), true); }); test('returns false when getUserMedia is unavailable', () => { setGlobal('window', { AudioContext: function AudioContext() {} }); setGlobal('AudioWorkletNode', function AudioWorkletNode() {}); setGlobal('navigator', {}); // no mediaDevices assert.equal(isMLDenoiseSupported(), false); }); test('returns false (does NOT throw) when the AudioWorkletNode binding is absent', () => { // Regression: a bare `!!AudioWorkletNode` threw ReferenceError on browsers // with AudioContext but no AudioWorkletNode; a detection helper must report // false, not throw. setGlobal('window', { AudioContext: function AudioContext() {} }); setGlobal('navigator', withMediaDevices); removeGlobal('AudioWorkletNode'); let result: boolean | undefined; assert.doesNotThrow(() => { result = isMLDenoiseSupported(); }); assert.equal(result, false); });