158 lines
6.2 KiB
TypeScript
158 lines
6.2 KiB
TypeScript
/**
|
|
* decodePressure hysteresis tests — the Part-1 auto-throttle signal logic.
|
|
*
|
|
* These cover the four named behaviours that make the visualizer-throttle safe: it engages only on
|
|
* SUSTAINED pressure, releases only after SUSTAINED recovery, never flaps on/off, and is a complete
|
|
* no-op when decode is healthy. The clock is injected so every transition is asserted at an exact
|
|
* timestamp — no real timers, fully deterministic.
|
|
*
|
|
* Run (no test runner configured; Node 22+ strips TS types natively — see OpusStreamDecoder.test.ts):
|
|
* dotnet build DeepDrftPublic/DeepDrftPublic.csproj
|
|
* cp DeepDrftPublic/Interop/audio/decodePressure.test.ts DeepDrftPublic/wwwroot/js/audio/
|
|
* node DeepDrftPublic/wwwroot/js/audio/decodePressure.test.ts
|
|
*
|
|
* A thrown error / non-zero exit signals failure; "ALL <n> TESTS PASSED" signals success.
|
|
*/
|
|
|
|
import {
|
|
DecodePressureSignal,
|
|
ENGAGE_EVENTS,
|
|
ENGAGE_WINDOW_MS,
|
|
RELEASE_QUIET_MS,
|
|
MIN_ENGAGED_MS,
|
|
} from './decodePressure.js';
|
|
|
|
// --- tiny inline harness (no dependencies) ---------------------------------------------------
|
|
let passed = 0;
|
|
const failures: string[] = [];
|
|
function test(name: string, fn: () => void): void {
|
|
try {
|
|
fn();
|
|
passed++;
|
|
} catch (e) {
|
|
failures.push(`FAIL: ${name}\n ${(e as Error).message}`);
|
|
}
|
|
}
|
|
function assertTrue(actual: boolean, msg?: string): void {
|
|
if (actual !== true) throw new Error(`${msg ?? 'assertTrue'}: expected true, got ${String(actual)}`);
|
|
}
|
|
function assertFalse(actual: boolean, msg?: string): void {
|
|
if (actual !== false) throw new Error(`${msg ?? 'assertFalse'}: expected false, got ${String(actual)}`);
|
|
}
|
|
|
|
/** A signal driven by a hand-advanced clock, so every transition is asserted at an exact time. */
|
|
function makeSignal() {
|
|
let now = 1000; // start at a non-zero base so "no prior stress" (-Infinity) is unambiguous
|
|
const sig = new DecodePressureSignal(() => now);
|
|
return {
|
|
sig,
|
|
at(ms: number) { now = ms; },
|
|
advance(ms: number) { now += ms; },
|
|
now() { return now; },
|
|
};
|
|
}
|
|
|
|
// --- no engage when healthy ------------------------------------------------------------------
|
|
|
|
test('healthy stream never engages (no reports at all)', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < 10; i++) {
|
|
advance(1000);
|
|
assertFalse(sig.isUnderPressure(), 'healthy must never be under pressure');
|
|
}
|
|
});
|
|
|
|
test('a single transient stress does not engage', () => {
|
|
const { sig, advance } = makeSignal();
|
|
sig.report();
|
|
assertFalse(sig.isUnderPressure(), 'one event is not sustained');
|
|
advance(500);
|
|
assertFalse(sig.isUnderPressure(), 'still not sustained');
|
|
});
|
|
|
|
test('fewer than ENGAGE_EVENTS within the window does not engage', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS - 1; i++) {
|
|
sig.report();
|
|
advance(10);
|
|
}
|
|
assertFalse(sig.isUnderPressure(), 'one short of the threshold must not engage');
|
|
});
|
|
|
|
test('stress spread wider than the window never accumulates enough to engage', () => {
|
|
const { sig, advance } = makeSignal();
|
|
// One report per full window: the prune drops each before the next, so the live count never
|
|
// reaches ENGAGE_EVENTS even after many reports.
|
|
for (let i = 0; i < ENGAGE_EVENTS * 3; i++) {
|
|
sig.report();
|
|
assertFalse(sig.isUnderPressure(), 'spread-out stress is not sustained');
|
|
advance(ENGAGE_WINDOW_MS);
|
|
}
|
|
});
|
|
|
|
// --- engages on sustained pressure -----------------------------------------------------------
|
|
|
|
test('ENGAGE_EVENTS within the window engages', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) {
|
|
sig.report();
|
|
advance(10); // all comfortably inside ENGAGE_WINDOW_MS
|
|
}
|
|
assertTrue(sig.isUnderPressure(), 'sustained pressure must engage');
|
|
});
|
|
|
|
// --- releases after recovery -----------------------------------------------------------------
|
|
|
|
test('releases after sustained quiet past the min dwell', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) { sig.report(); advance(10); }
|
|
assertTrue(sig.isUnderPressure(), 'engaged');
|
|
|
|
// Quiet long enough to satisfy BOTH the min engaged dwell and the release-quiet window.
|
|
advance(Math.max(MIN_ENGAGED_MS, RELEASE_QUIET_MS) + 1);
|
|
assertFalse(sig.isUnderPressure(), 'sustained recovery must release');
|
|
});
|
|
|
|
test('re-engages after a release when a fresh burst arrives', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) { sig.report(); advance(10); }
|
|
assertTrue(sig.isUnderPressure(), 'engaged first time');
|
|
advance(Math.max(MIN_ENGAGED_MS, RELEASE_QUIET_MS) + 1);
|
|
assertFalse(sig.isUnderPressure(), 'released');
|
|
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) { sig.report(); advance(10); }
|
|
assertTrue(sig.isUnderPressure(), 'a fresh sustained burst re-engages');
|
|
});
|
|
|
|
// --- no flap ---------------------------------------------------------------------------------
|
|
|
|
test('stays engaged during a brief quiet shorter than the release window', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) { sig.report(); advance(10); }
|
|
assertTrue(sig.isUnderPressure(), 'engaged');
|
|
|
|
// A gap shorter than RELEASE_QUIET_MS must NOT release — that is the anti-flap guarantee.
|
|
advance(RELEASE_QUIET_MS - 100);
|
|
assertTrue(sig.isUnderPressure(), 'a brief quiet must not drop the throttle');
|
|
});
|
|
|
|
test('continued stress holds the throttle engaged indefinitely', () => {
|
|
const { sig, advance } = makeSignal();
|
|
for (let i = 0; i < ENGAGE_EVENTS; i++) { sig.report(); advance(10); }
|
|
assertTrue(sig.isUnderPressure(), 'engaged');
|
|
|
|
// Keep reporting at a cadence under the release window; it must never release.
|
|
for (let i = 0; i < 20; i++) {
|
|
advance(RELEASE_QUIET_MS - 100);
|
|
sig.report();
|
|
assertTrue(sig.isUnderPressure(), 'ongoing stress keeps it engaged');
|
|
}
|
|
});
|
|
|
|
// --- report ----------------------------------------------------------------------------------
|
|
if (failures.length > 0) {
|
|
console.error(failures.join('\n'));
|
|
throw new Error(`${failures.length} test(s) failed, ${passed} passed`);
|
|
}
|
|
console.log(`ALL ${passed} TESTS PASSED`);
|