353bb59393
- scheduledMessages.test.ts (9): pins the MSC4140 request shape (PUT to the room send endpoint with the org.matrix.msc4140.delay query, POST cancel/restart to /delayed_events with the unstable prefix), the delay-floor math (Math.max(1000, round(sendAt-now)) — "now"/past targets still yield a valid >=1000ms delay), rounding, and url-encoding. - lotusDenoiseUtils.test.ts (9): model-catalog data integrity + isMLDenoiseSupported feature detection across AudioContext/webkit/getUserMedia. - Bug found + fixed: isMLDenoiseSupported used `!!AudioWorkletNode`, a bare global reference that throws ReferenceError (not returns false) on a browser with AudioContext but no AudioWorkletNode binding. Switched to `typeof` so the detection helper reports unsupported instead of throwing. Regression test proven to fail on the old code. Suite now 545 tests (4th real bug caught by the prevention work). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
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<string, unknown>;
|
|
const NAMES = ['window', 'navigator', 'AudioWorkletNode'] as const;
|
|
let saved: Record<string, PropertyDescriptor | undefined>;
|
|
|
|
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);
|
|
});
|