Files

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`);