feedBack/tests/js/renderer_bus_feeder.test.js
Byron Gamatos 8d0e270345
refactor(app): carve the JUCE/desktop audio shims out of app.js (R3a) (#893)
* refactor(app): carve resume-session out of app.js (R3a)

static/js/resume-session.js (157) — the snapshot taken when you leave a song and the
pill that offers it back. Bodies VERBATIM. app.js 7,727 → 7,601.
Fifth slice out of the strongly-connected core. ONE hook (playSong) + a
currentFilename getter.

S.pendingResume JOINS THE CONTAINER — on demand, exactly as intended. app.js WRITES
it (playSong({ resume }) arms it; the song:ready listener consumes it) while this
module reads it, so it cannot be a plain export: an imported binding is read-only.
Same reason isPlaying is there. The container grows one field per carve that needs
it, never speculatively.

THE CONTRACT TEST CAUGHT THE MISSING HOOK, again on a path nothing executes:
"playSong is read by a module but never wired by app.js — it would throw at runtime".
Second time it has caught a real wiring gap the moment it appeared.

A REAL TRAP, worth remembering: I first did the S.pendingResume rewrite by feeding
acorn's identifier RANGES from node into python, and it corrupted the file
(`_pS.pendingResume null;`). **Acorn's offsets are UTF-16 code units; Python's string
indices are code points.** static/app.js contains emoji, so every offset past one
drifts. Do an AST-driven rewrite in the SAME language that produced the offsets.
`node --check` caught it; a silent version of that bug is very easy to imagine.

VERIFIED. A/B against origin/main in two browsers, real song: the window API
(resumeLastSession / _snapshotResumeSession / _readResumeSession /
_clearResumeSession), snapshot, read-back, and clear — IDENTICAL, zero page errors.
HONEST LIMIT: my probe never got the snapshot to actually PERSIST (there is a guard
beyond the 3s minimum position that a scripted playSong does not satisfy), so that
path is verified only as identical-to-main, not as observed-working. The real
coverage is tests/browser/resume-session.spec.ts, which drives the flow properly.

Zero harnesses broke. pytest 2396, node 1040/1040, ESLint 0 (no-cycle clean),
tailwind clean, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(app): carve the JUCE/desktop audio shims out of app.js (R3a)

static/js/juce-audio.js (994) — bodies VERBATIM. app.js 7,603 → 6,643.
THE LARGEST SINGLE SLICE of the whole carve phase: 960 lines, ~13% of what was left.

Three self-installing IIFEs:
  _installJuceEngineRoutingWatcher (444)  routes a song to the JUCE engine or HTML5 as
                                          the desktop output enters/leaves exclusive/ASIO
  _installRendererBusFeeder        (337)  feeds the highway renderer bus from whichever
                                          transport is actually running
  _installJuceAudioElementShim     (156)  patches audio.play/pause so the rest of the app
                                          keeps talking to the <audio> element while JUCE
                                          owns the transport

They EXPORT NOTHING — all three publish through `window.*` (_juceMode,
_reevaluateJuceRouting, _reevaluateRendererBus, …). So app.js needs only a
side-effect import plus the one binding it actually uses
(_resetJuceAudioShimChain, which the shim IIFE assigns).

THE ORDERING QUESTION, CHECKED RATHER THAN ASSUMED. Importing this module runs the
IIFEs EARLIER than before: imports evaluate ahead of app.js's body, and therefore
ahead of configureHost(). A hook read at IIFE-execution time would THROW. So I walked
the AST at IIFE-body depth to see what they actually touch when they run: nothing but
listener registration, and `audio.play`/`audio.pause` patching — and `audio` is itself
an imported module now. Verified in the browser: both are patched on the carved build
exactly as on main, which proves the shim installs correctly at its new, earlier point.
(Had I got this wrong, host.js throws loudly rather than silently misbehaving — which
is the whole reason it has no no-op defaults.)

VERIFIED. A/B against origin/main in two browsers: the entire window.* surface the
IIFEs publish (_juceMode, _juceOutputIsExclusive, _reevaluateJuceRouting,
_reevaluateRendererBus, _clearJuceRerouteMemo), audio.play/pause patched, a real song
loading and togglePlay driving the public mirror — IDENTICAL, zero page errors.

Harnesses: juce_engine_reroute (19 tests) + renderer_bus_feeder (13) slice the IIFEs by
signature — retargeted, and each sandbox gains a `host` object routed at its EXISTING
stubs so every assertion holds unchanged. test_plugin_runtime_idempotence is SPLIT: 3 of
its 4 source-asserts stayed in app.js, the sm.emit('song:resume') one moved.

pytest 2396, node 1040/1040, ESLint 0 (no-cycle clean), tailwind clean, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:44:03 +02:00

343 lines
15 KiB
JavaScript

// Behavioral tests for the renderer-audio bus feeder in static/js/juce-audio.js.
//
// The feeder (an IIFE, `_installRendererBusFeeder`) captures renderer-side
// song audio (stems-plugin WebAudio master, or the core <audio> element) and
// pushes it into the desktop engine's renderer bus while the output device is
// exclusive-style — the Phase 2 path for audio the native backing transport
// cannot carry. These tests extract that IIFE from source and exercise
// `window._reevaluateRendererBus` against fakes, covering: stems engagement
// under exclusive output, disengagement on return to shared mode, inertness
// in shared mode / while the native transport owns the song, and the
// element-capture fallback.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
// The JUCE audio shims were carved out of app.js into their own module (R3a).
const APP_JS = path.join(__dirname, '..', '..', 'static', 'js', 'juce-audio.js');
function extractFeederIIFE(src) {
const marker = '(function _installRendererBusFeeder() {';
const start = src.indexOf(marker);
assert.ok(start !== -1, 'feeder IIFE not found in static/js/juce-audio.js');
const openBrace = src.indexOf('{', start);
let depth = 1;
let i = openBrace + 1;
while (i < src.length && depth > 0) {
const ch = src[i];
if (ch === '{') depth++;
else if (ch === '}') depth--;
i++;
}
assert.ok(depth === 0, 'unbalanced braces in feeder IIFE');
const tail = src.slice(i, i + 5);
assert.match(tail, /^\)\(\)/, 'feeder IIFE not immediately invoked');
return src.slice(start, i) + ')();';
}
function makeFakeContext(sampleRate = 48000) {
const ctx = {
sampleRate,
state: 'running',
sinkIdCalls: [],
destination: { isDestination: true },
setSinkId(v) { this.sinkIdCalls.push(v); return Promise.resolve(); },
resume() { this.state = 'running'; return Promise.resolve(); },
audioWorklet: { addModule: () => Promise.resolve() },
createMediaElementSource(el) {
this.mediaSourceEl = el;
return { connect() {}, disconnect() {} };
},
createMediaStreamSource(stream) {
this.mediaStreamSource = stream;
return { connect() {}, disconnect() {} };
},
close() { this.closed = true; return Promise.resolve(); },
};
return ctx;
}
// Fake getDisplayMedia stream for the loopback-capture path.
function makeLoopbackStream({ suppressed = true } = {}) {
const stopped = [];
const audioTrack = {
kind: 'audio',
stop() { stopped.push('audio'); },
getSettings: () => (suppressed ? { suppressLocalAudioPlayback: true } : {}),
};
const videoTrack = { kind: 'video', stop() { stopped.push('video'); } };
return {
__stopped: stopped,
getAudioTracks: () => [audioTrack],
getVideoTracks: () => [videoTrack],
getTracks: () => [videoTrack, audioTrack],
};
}
// `displayMedia`: undefined → loopback capture unavailable (Docker sphere /
// old desktop main); a function → used as navigator.mediaDevices.getDisplayMedia.
function makeSandbox({ isAudioRunning = () => true, exclusive = () => true, displayMedia } = {}) {
const calls = { setRendererBus: [], pushRendererAudio: [], setPageMuted: [] };
const api = {
isAudioRunning: () => Promise.resolve(isAudioRunning()),
setRendererBus: (en, g) => { calls.setRendererBus.push([en, g]); return Promise.resolve(); },
pushRendererAudio: (buf, rate) => { calls.pushRendererAudio.push([buf.length, rate]); },
setPageMuted: (m) => { calls.setPageMuted.push(m); return Promise.resolve(m); },
};
class FakeWorkletNode {
constructor() { this.port = { onmessage: null }; }
connect() {}
disconnect() {}
}
const sandbox = {
console: { log() {}, warn() {}, error() {} },
URL: { createObjectURL: () => 'blob:tap', revokeObjectURL() {} },
Blob: class { constructor() {} },
AudioWorkletNode: FakeWorkletNode,
AudioContext: function () { const c = makeFakeContext(); sandbox.__createdContexts.push(c); return c; },
WeakSet, WeakMap, Promise, Float32Array, Math,
setInterval: () => 0,
document: {
hidden: false,
addEventListener() {},
getElementById: () => sandbox.__audioEl,
},
__createdContexts: [],
__audioEl: { id: 'audio' },
__calls: calls,
navigator: { mediaDevices: displayMedia ? { getDisplayMedia: displayMedia } : {} },
window: null,
};
sandbox.window = {
feedBackDesktop: { audio: api },
_juceOutputIsExclusive: () => Promise.resolve(exclusive()),
_juceMode: false,
_currentSongAudio: null,
feedBack: { stems: {} },
};
sandbox.globalThis = sandbox;
const src = fs.readFileSync(APP_JS, 'utf8');
// The shims reach back into app.js through the host seam (static/js/host.js).
// Route it at the SAME stubs this sandbox already had — a fresh `() => {}` would
// swallow the calls and the assertions below would pass vacuously.
sandbox.host = {
jucePlayer: () => sandbox.jucePlayer,
playSong: (...a) => (sandbox.playSong ? sandbox.playSong(...a) : undefined),
_audioSeek: (...a) => (sandbox._audioSeek ? sandbox._audioSeek(...a) : Promise.resolve({ completed: true })),
setPlayButtonState: (...a) => (sandbox.setPlayButtonState ? sandbox.setPlayButtonState(...a) : undefined),
_songEventPayload: (...a) => (sandbox._songEventPayload ? sandbox._songEventPayload(...a) : ({})),
showScreen: (...a) => (sandbox.showScreen ? sandbox.showScreen(...a) : undefined),
};
vm.createContext(sandbox);
vm.runInContext(extractFeederIIFE(src), sandbox);
assert.equal(typeof sandbox.window._reevaluateRendererBus, 'function',
'feeder must expose window._reevaluateRendererBus');
return sandbox;
}
function makeStemsGraph() {
return {
context: makeFakeContext(),
masterNode: { connect() {}, disconnect() {} },
};
}
// Surface-mode (stems/element) tests run WITHOUT getDisplayMedia: the first
// tick probes loopback, fails, and latches _loopbackUnavailable; the second
// tick exercises the fallback surface mode. This mirrors an old desktop main
// without the display-media handler.
async function reevaluateWithFallback(sb) {
await sb.window._reevaluateRendererBus(); // loopback probe → unavailable
await sb.window._reevaluateRendererBus(); // surface fallback
}
test('stems graph + exclusive output → bus enabled, stems ctx null-sinked (loopback unavailable)', async () => {
const sb = makeSandbox({ exclusive: () => true });
const graph = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = graph;
await reevaluateWithFallback(sb);
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 'bus enabled');
assert.equal(graph.context.sinkIdCalls.at(-1)?.type, 'none', 'stems ctx re-pointed at null sink');
});
test('output returns to shared → bus disabled, sink restored', async () => {
let excl = true;
const sb = makeSandbox({ exclusive: () => excl });
const graph = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = graph;
await reevaluateWithFallback(sb);
excl = false;
await sb.window._reevaluateRendererBus();
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [false, 0], 'bus disabled');
assert.equal(graph.context.sinkIdCalls.at(-1), '', 'default sink restored');
});
test('stems graph + shared output → feeder stays off (no double audio)', async () => {
const sb = makeSandbox({ exclusive: () => false });
sb.window.feedBack.stems.audioGraph = makeStemsGraph();
await sb.window._reevaluateRendererBus();
assert.equal(sb.__calls.setRendererBus.length, 0, 'bus never touched in shared mode');
});
test('element song + exclusive → element captured into bus (loopback unavailable)', async () => {
const sb = makeSandbox({ exclusive: () => true });
sb.window._currentSongAudio = { url: '/api/sloppak/x.sloppak/file/stems/full.ogg' };
sb.window._juceMode = false;
await reevaluateWithFallback(sb);
assert.equal(sb.__createdContexts.length, 1, 'capture context created');
assert.equal(sb.__createdContexts[0].mediaSourceEl, sb.__audioEl, 'element source captured');
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 'bus enabled');
});
test('native-transport song, loopback unavailable → surface modes stay off', async () => {
const sb = makeSandbox({ exclusive: () => true });
sb.window._currentSongAudio = { url: '/audio/song.ogg' };
sb.window._juceMode = true;
await reevaluateWithFallback(sb);
assert.ok(!sb.__calls.setRendererBus.some(([en]) => en === true),
'bus never ENABLED (failed-probe cleanup may disable it)');
assert.equal(sb.__createdContexts.length, 0, 'no capture context created');
});
test('stems graph replaced mid-engagement → re-engages on the new graph', async () => {
const sb = makeSandbox({ exclusive: () => true });
const g1 = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = g1;
await reevaluateWithFallback(sb);
const g2 = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = g2;
await sb.window._reevaluateRendererBus();
assert.equal(g2.context.sinkIdCalls.at(-1)?.type, 'none', 'new graph null-sinked');
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 're-enabled for new graph');
});
// ── Loopback mode (whole-app capture) ────────────────────────────────────────
test('exclusive output + loopback available → engages without any song loaded', async () => {
const stream = makeLoopbackStream();
const sb = makeSandbox({ exclusive: () => true, displayMedia: () => Promise.resolve(stream) });
await sb.window._reevaluateRendererBus();
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 'bus enabled for whole session');
assert.ok(stream.__stopped.includes('video'), 'unused video track stopped');
assert.equal(sb.__createdContexts.at(-1)?.mediaStreamSource, stream, 'loopback stream captured');
assert.equal(sb.__calls.setPageMuted.length, 0, 'suppress constraint honoured — no page mute');
});
test('loopback context is closed on disengage (no orphaned tap worklet)', async () => {
let excl = true;
const stream = makeLoopbackStream();
const sb = makeSandbox({ exclusive: () => excl, displayMedia: () => Promise.resolve(stream) });
await sb.window._reevaluateRendererBus(); // engage loopback
const lbCtx = sb.__createdContexts.at(-1);
assert.equal(lbCtx?.mediaStreamSource, stream, 'loopback engaged');
assert.notEqual(lbCtx.closed, true, 'context live while engaged');
excl = false;
await sb.window._reevaluateRendererBus(); // disengage
assert.equal(lbCtx.closed, true, 'loopback context closed on disengage');
assert.ok(stream.__stopped.includes('audio'), 'capture stream stopped');
});
test('loopback preferred over stems when both available', async () => {
const stream = makeLoopbackStream();
const sb = makeSandbox({ exclusive: () => true, displayMedia: () => Promise.resolve(stream) });
const graph = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = graph;
await sb.window._reevaluateRendererBus();
assert.equal(graph.context.sinkIdCalls.length, 0, 'stems ctx untouched — loopback owns capture');
assert.equal(sb.__createdContexts.at(-1)?.mediaStreamSource, stream, 'loopback engaged');
});
test('suppressLocalAudioPlayback unsupported → page-mute fallback, unmuted on disengage', async () => {
let excl = true;
const stream = makeLoopbackStream({ suppressed: false });
const sb = makeSandbox({ exclusive: () => excl, displayMedia: () => Promise.resolve(stream) });
await sb.window._reevaluateRendererBus();
assert.deepEqual(sb.__calls.setPageMuted, [true], 'page muted as fallback');
excl = false;
await sb.window._reevaluateRendererBus();
assert.deepEqual(sb.__calls.setPageMuted, [true, false], 'page unmuted on disengage');
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [false, 0], 'bus disabled');
});
test('getDisplayMedia rejected → sticky fallback to surface modes', async () => {
const sb = makeSandbox({
exclusive: () => true,
displayMedia: () => Promise.reject(new DOMException('denied', 'NotAllowedError')),
});
const graph = makeStemsGraph();
sb.window.feedBack.stems.audioGraph = graph;
await sb.window._reevaluateRendererBus(); // probe fails, latches unavailable
await sb.window._reevaluateRendererBus(); // falls back to stems
assert.equal(graph.context.sinkIdCalls.at(-1)?.type, 'none', 'stems fallback engaged');
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 'bus enabled via fallback');
});
test('element capture collision (createMediaElementSource throws) → no poisoned state, clean retry', async () => {
const sb = makeSandbox({ exclusive: () => true }); // loopback unavailable
sb.window._currentSongAudio = { url: '/api/sloppak/x.sloppak/file/stems/full.ogg' };
// First capture attempt collides (highway analyser owns the element).
let collide = true;
const origFactory = sb.AudioContext;
sb.__createdContexts.length = 0;
// Patch contexts so createMediaElementSource throws while colliding.
sb.AudioContext = function () {
const c = origFactory();
const orig = c.createMediaElementSource.bind(c);
c.createMediaElementSource = (el) => {
if (collide) throw new DOMException('already connected', 'InvalidStateError');
return orig(el);
};
c.close = () => Promise.resolve();
return c;
};
await reevaluateWithFallback(sb); // element engage fails (collision)
assert.ok(!sb.__calls.setRendererBus.some(([en]) => en === true), 'bus never left enabled');
collide = false;
await sb.window._reevaluateRendererBus(); // retry succeeds — no TypeError, fresh ctx
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [true, 1.0], 'element engaged after collision cleared');
});
test('engine stops → bus disabled', async () => {
let running = true;
const sb = makeSandbox({ isAudioRunning: () => running, exclusive: () => true });
sb.window.feedBack.stems.audioGraph = makeStemsGraph();
await sb.window._reevaluateRendererBus();
running = false;
await sb.window._reevaluateRendererBus();
assert.deepEqual(sb.__calls.setRendererBus.at(-1), [false, 0], 'bus disabled after engine stop');
});