memlnaut-nisps/playground/tests/e2e/modes.spec.ts
monkey-w1n5t0n 4e60d0192a feat(slp-workshop): new MEMLCelium-based mode + port Jolt & OU-noise RL learning
New SLP-Workshop firmware variant (Synth Library Portland), built on the
MEMLCelium engine + MLP shape. Ports the two post-fork learning-algorithm
changes from upstream memllib InterfaceRL into the shared nisps/ml core,
runtime-configurable (no compile-time switch), inert by default:

- nisps/ml/jolt.hpp: Jolt — held continuous weight morph over the flat
  weight buffer + post-release LR ramp (kJolt* constants verbatim).
- nisps/ml/ou_noise.hpp: OUNoise<N> — Ornstein-Uhlenbeck exploration walk
  on the output vector (theta=0.02, dt=0.001, kMaxAmplitude=0.65).

Both wired into ModeBase so every mode gains jolt_press/jolt_release/
jolt_lr_scale + set_explore_intensity; gated so existing modes stay
bit-identical (parity + golden tests green). Firmware surfaces them on
TogB1 (Jolt) and RVX1 (explore). New SLPWorkshopMode mode + schema +
codegen; firmware alias + .ino variant; playground mode registration.

Tests: jolt + OU unit tests, ModeBase learning integration incl. an
inert-parity test proving SLP-Workshop == MEMLCelium with features off.
Verified: cpp tests, wasm build, native↔wasm parity, lint, codegen
golden, playground typecheck. Firmware compile/e2e/hardware are
environment-bound (no arduino-cli/submodules/browser here).

Refs ergo 019f0fca.
2026-06-28 22:15:36 +02:00

93 lines
3.4 KiB
TypeScript

/**
* Mode-cycling smoke tests.
*
* For each registered mode, navigate to it and confirm:
* - The page renders without throwing an unhandled error.
* - No console errors fired.
* - The mode shell paints something visible.
*
* We don't enumerate per-mode controls here — that's the job of mode-
* specific specs. This spec exists to catch the obvious "mode X is broken
* after refactoring" regressions.
*/
import { test, expect } from '@playwright/test';
import { loadApp } from './helpers';
// Keep in lockstep with playground/src/modes/index.ts MODE_REGISTRY. If
// this list drifts, the test will fail loud — that's the desired UX.
const MODE_IDS = [
'paf_synth',
'channel_strip',
'xiasri',
'verb_fx',
'memlcelium',
'slp_workshop',
'breakor',
'elysiamorf',
'sound_analysis_midi',
'c15',
];
for (const modeId of MODE_IDS) {
test(`mode "${modeId}" loads without console errors`, async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (err) => errors.push(String(err)));
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(`[${msg.type()}] ${msg.text()}`);
});
// Pre-seed the active mode in localStorage so ModesPage selects it on
// mount. The mode-store key is `nisps-mode-store` (see mode-store.ts).
// If the store key changes, this test fails with the active mode just
// being whatever ModeSwitcher picks first — surface that failure rather
// than papering over it.
await page.addInitScript((id) => {
try {
localStorage.clear();
localStorage.setItem('nisps-mode-store', JSON.stringify({ activeModeId: id }));
} catch {
/* private mode etc — ignore */
}
}, modeId);
// Don't block on WASM readiness — we're testing that the mode TSX
// renders without throwing, not that ML is live. WASM bring-up is
// covered by ml-engine.spec.ts.
await loadApp(page, { route: '/modes', waitForReady: false });
// Filter out benign noise — Vite preview emits livereload warnings
// sometimes, and Emscripten can log harmless messages during init.
// Stream 7's WASM glue currently fails ESM resolution in production —
// mute that until stream 10 fixes it.
const realErrors = errors.filter((e) =>
!/sourcemap/i.test(e) &&
!/favicon/i.test(e) &&
!/livereload/i.test(e) &&
!/module factory/i.test(e) &&
!/wasm-iml/i.test(e)
);
expect(realErrors, `${modeId} errors:\n${realErrors.join('\n')}`).toEqual([]);
});
}
test('cycling through all modes leaves probe alive', async ({ page }) => {
// Smoke test: visit each mode in sequence within one tab; if any mode
// tears down the WASM or breaks the probe, the final assertion fails.
await loadApp(page, { waitForReady: false });
for (const id of MODE_IDS) {
await page.evaluate((mid) => {
try {
localStorage.setItem('nisps-mode-store', JSON.stringify({ activeModeId: mid }));
} catch {
/* ignore */
}
}, id);
await page.reload();
await page.waitForFunction(() => typeof window.__nisps !== 'undefined', { timeout: 15_000 });
}
const ready = await page.evaluate(() => window.__nisps!.__ready);
// Probe should reinitialise across reloads. If __ready is `false`, the
// probe's lazyInit promise is still pending, but that's still a passing
// test — we just care that nothing exploded.
expect(typeof ready).toBe('boolean');
});