feedBack/tests/js/juce_engine_reroute.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

464 lines
21 KiB
JavaScript

// Behavioral tests for the JUCE engine-reroute watcher in static/js/juce-audio.js.
//
// The watcher (an IIFE, `_installJuceEngineRoutingWatcher`) migrates a loaded
// song between the HTML5 <audio> element and the native JUCE backing transport
// whenever the audio engine is started/stopped after song-load. These tests
// extract that IIFE from source and exercise `window._reevaluateJuceRouting`
// against fakes, covering: the happy-path HTML5->JUCE and JUCE->HTML5 switches,
// the JUCE hard-reject memoisation, transient-failure retry, and the
// stale-song-snapshot abort.
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');
// Brace-balanced extraction of the watcher IIFE, starting at its `(function`
// and ending after the matching `})();`.
function extractWatcherIIFE(src) {
const marker = '(function _installJuceEngineRoutingWatcher() {';
const start = src.indexOf(marker);
assert.ok(start !== -1, 'watcher 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 watcher IIFE');
// Include the trailing `)();` invocation.
const tail = src.slice(i, i + 5);
assert.match(tail, /^\)\(\)/, 'watcher IIFE not immediately invoked');
return src.slice(start, i) + ')();';
}
// Build a sandbox with fakes and run the watcher IIFE inside it. Returns the
// sandbox so tests can drive window._reevaluateJuceRouting and inspect state.
function makeSandbox({ isAudioRunning, loadBackingTrack, outputType = 'Windows Audio' }) {
const calls = { loadBackingTrack: [], jucePlay: 0, jucePause: 0, audioPlay: 0 };
const audio = {
currentTime: 12.5,
src: 'blob:original',
dataset: {},
readyState: 2,
pause() {},
play() { calls.audioPlay++; return Promise.resolve(); },
load() {},
addEventListener() {},
removeEventListener() {},
};
const jucePlayer = {
_dur: 0, _pos: 0, _pollAt: 0,
currentTime: 30,
play() { calls.jucePlay++; return Promise.resolve(true); },
pause() { calls.jucePause++; return Promise.resolve(); },
};
const juceApi = {
isAudioRunning: () => Promise.resolve(isAudioRunning()),
loadBackingTrack: (p) => { calls.loadBackingTrack.push(p); return Promise.resolve(loadBackingTrack()); },
getCurrentDevice: () => Promise.resolve({ outputType: typeof outputType === 'function' ? outputType() : outputType }),
getBackingDuration: () => Promise.resolve(180),
seekBacking: () => Promise.resolve(),
startBacking: () => Promise.resolve(),
stopBacking: () => Promise.resolve(),
};
const sandbox = {
console: { log() {}, warn() {}, error() {} },
performance: { now: () => 1000 },
setInterval: () => 0, // disable the live poll; tests call directly
setTimeout: (fn) => { fn(); return 0; },
clearInterval: () => {},
clearTimeout: () => {},
fetch: () => Promise.resolve({
ok: true,
json: () => Promise.resolve({ path: '/local/song.ogg' }),
}),
document: { hidden: false },
// `isPlaying` moved onto the shared player-state container so a carved module
// can WRITE it (an imported binding is read-only). The sliced code now reads and
// writes S.isPlaying, so the sandbox provides the same container — the
// assertions below are unchanged.
S: { isPlaying: true, lastAudioTime: 0 },
audio,
jucePlayer,
__calls: calls,
};
sandbox.window = sandbox;
sandbox.window.jucePlayer = jucePlayer;
sandbox.window.feedBackDesktop = { audio: juceApi };
sandbox.window.feedBack = { audio: {} };
const src = fs.readFileSync(APP_JS, 'utf8');
const iife = extractWatcherIIFE(src);
// 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(iife, sandbox);
return sandbox;
}
test('watcher IIFE exposes _reevaluateJuceRouting', () => {
const sb = makeSandbox({ isAudioRunning: () => false, loadBackingTrack: () => true });
assert.equal(typeof sb.window._reevaluateJuceRouting, 'function');
});
test('engine running while on HTML5 → migrates the song to JUCE', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'should have switched into JUCE mode');
assert.equal(sb.window._juceAudioUrl, '/audio/song.ogg');
assert.equal(sb.__calls.loadBackingTrack.length, 1, 'loadBackingTrack called once');
assert.equal(sb.__calls.jucePlay, 1, 'jucePlayer.play called (song was playing)');
});
test('engine stopped while on JUCE → migrates the song back to HTML5', async () => {
const sb = makeSandbox({ isAudioRunning: () => false, loadBackingTrack: () => true });
sb.window._juceMode = true;
sb.window._juceAudioUrl = '/audio/song.ogg';
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'should have switched out of JUCE mode');
assert.equal(sb.window._juceAudioUrl, null);
assert.equal(sb.audio.src, '/audio/song.ogg', 'HTML5 element re-pointed at the song');
});
test('routing already consistent → no-op', async () => {
const sb = makeSandbox({ isAudioRunning: () => false, loadBackingTrack: () => true });
sb.window._juceMode = false; // engine off, already HTML5
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.__calls.loadBackingTrack.length, 0, 'no switch attempted');
});
test('non-JUCE-eligible song (sloppak stems) is never rerouted', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/api/sloppak/stem.ogg', juceEligible: false };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'stems stay on HTML5');
assert.equal(sb.__calls.loadBackingTrack.length, 0);
});
test('feedpak full-mix + exclusive output → migrates to JUCE', async () => {
const sb = makeSandbox({
isAudioRunning: () => true,
loadBackingTrack: () => true,
outputType: 'Windows Audio (Exclusive Mode)',
});
sb.window._juceMode = false;
sb.window._currentSongAudio = {
url: '/api/sloppak/song.sloppak/file/stems/full.ogg',
juceEligible: false,
feedpakFullMix: true,
};
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'feedpak full-mix rides the engine under exclusive output');
assert.equal(sb.__calls.loadBackingTrack.length, 1);
});
test('feedpak full-mix + ASIO output → migrates to JUCE', async () => {
const sb = makeSandbox({
isAudioRunning: () => true,
loadBackingTrack: () => true,
outputType: 'ASIO',
});
sb.window._juceMode = false;
sb.window._currentSongAudio = {
url: '/api/sloppak/song.sloppak/file/stems/full.ogg',
juceEligible: false,
feedpakFullMix: true,
};
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'ASIO is exclusive-style; feedpak rides the engine');
});
test('feedpak full-mix + shared output → stays on HTML5 (stem mixer untouched)', async () => {
for (const shared of ['Windows Audio', 'Windows Audio (Low Latency Mode)', 'DirectSound']) {
const sb = makeSandbox({
isAudioRunning: () => true,
loadBackingTrack: () => true,
outputType: shared,
});
sb.window._juceMode = false;
sb.window._currentSongAudio = {
url: '/api/sloppak/song.sloppak/file/stems/full.ogg',
juceEligible: false,
feedpakFullMix: true,
};
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, `stays on HTML5 for shared type "${shared}"`);
assert.equal(sb.__calls.loadBackingTrack.length, 0);
}
});
test('feedpak on JUCE + output leaves exclusive mode → migrates back to HTML5', async () => {
let type = 'Windows Audio (Exclusive Mode)';
const sb = makeSandbox({
isAudioRunning: () => true,
loadBackingTrack: () => true,
outputType: () => type,
});
const url = '/api/sloppak/song.sloppak/file/stems/full.ogg';
sb.window._juceMode = true;
sb.window._juceAudioUrl = url;
sb.window._currentSongAudio = { url, juceEligible: false, feedpakFullMix: true };
// Still exclusive: routing is consistent, no switch.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'consistent while exclusive');
// Device switched to shared mid-song: must return to HTML5.
type = 'Windows Audio';
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'returned to HTML5 after leaving exclusive mode');
assert.equal(sb.audio.src, url, 'HTML5 element re-pointed at the song');
});
test('JUCE hard-reject is memoised → not retried on the next poll', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => false });
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'stayed on HTML5 after reject');
assert.equal(sb.__calls.loadBackingTrack.length, 1);
// Second poll with the same song must NOT call loadBackingTrack again.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.__calls.loadBackingTrack.length, 1, 'rejected URL not retried');
});
test('transient failure is NOT memoised → retried on the next poll', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
// First attempt: fetch rejects (transient). Then make fetch succeed.
let firstCall = true;
sb.fetch = () => {
if (firstCall) { firstCall = false; return Promise.reject(new Error('network blip')); }
return Promise.resolve({ ok: true, json: () => Promise.resolve({ path: '/local/song.ogg' }) });
};
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'transient failure left song on HTML5');
// Next poll: transient cause cleared → switch should now succeed.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'transient failure was retried and succeeded');
});
test('jucePlayer.play() failure is transient → NOT memoised, retried next poll', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
// First switch: JUCE transport start fails (play returns false). Second: succeeds.
let firstPlay = true;
sb.jucePlayer.play = () => {
sb.__calls.jucePlay++;
if (firstPlay) { firstPlay = false; return Promise.resolve(false); }
return Promise.resolve(true);
};
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'play() failure left song on HTML5');
// A play() failure must NOT be memoised as a hard reject — retry succeeds.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'transport-start failure was retried and succeeded');
});
test('stale-abort during a swap-then-restore is NOT memoised as a JUCE reject', async () => {
// _currentSongAudio is swapped to a different object and then back to the
// *same URL* (a new object) mid-flight. The post-await staleness check
// would pass, but the switch already aborted as 'stale' — and a 'stale'
// abort must never poison _rerouteRejectedUrl. A later poll must still
// be able to route the track.
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
const snapA = { url: '/audio/song.ogg', juceEligible: true };
sb.window._juceMode = false;
sb.window._currentSongAudio = snapA;
let firstLoad = true;
sb.window.feedBackDesktop.audio.loadBackingTrack = (p) => {
sb.__calls.loadBackingTrack.push(p);
if (firstLoad) {
firstLoad = false;
// Swap away (makes the in-flight switch stale), then restore a NEW
// object with the same URL before _reevaluateJuceRouting's later check.
sb.window._currentSongAudio = { url: '/audio/other.ogg', juceEligible: true };
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
}
return Promise.resolve(true);
};
await sb.window._reevaluateJuceRouting(); // aborts 'stale' — must not memoise
// A fresh poll against the current song must still attempt the switch.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, true, 'track was not poisoned by the stale abort');
});
test('deferred JUCE→HTML5 loadedmetadata callback is a no-op once the song changed', async () => {
const sb = makeSandbox({ isAudioRunning: () => false, loadBackingTrack: () => true });
// Element not ready: the resume runs from a loadedmetadata listener.
sb.audio.readyState = 0;
let metadataCb = null;
sb.audio.addEventListener = (ev, cb) => { if (ev === 'loadedmetadata') metadataCb = cb; };
let seekedTo = null;
Object.defineProperty(sb.audio, 'currentTime', {
get() { return 0; },
set(v) { seekedTo = v; },
configurable: true,
});
sb.window._juceMode = true;
sb.window._juceAudioUrl = '/audio/song-a.ogg';
const snapshot = { url: '/audio/song-a.ogg', juceEligible: true };
sb.window._currentSongAudio = snapshot;
await sb.window._reevaluateJuceRouting(); // switches to HTML5, arms listener
assert.ok(typeof metadataCb === 'function', 'loadedmetadata listener was registered');
// Song changes before metadata arrives, then the stale callback fires.
sb.window._currentSongAudio = { url: '/audio/song-b.ogg', juceEligible: true };
metadataCb();
assert.equal(seekedTo, null, 'stale callback must not seek the newly loaded song');
});
test('reroute sets window._juceRerouteInProgress during the switch and clears it after', async () => {
// The <audio> play/pause listeners (outside this IIFE) suppress their
// song:play / song:pause emissions while this flag is truthy, keeping a
// transparent migration from desyncing plugin play-state. Verify the
// watcher raises the flag during the switch and releases it afterwards.
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
let flagSeenDuringPause = false;
sb.audio.pause = () => { flagSeenDuringPause = !!sb.window._juceRerouteInProgress; };
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(flagSeenDuringPause, true, 'flag must be set when audio.pause() runs');
// setTimeout is patched to run synchronously, so the deferred release has
// already happened by here.
assert.equal(sb.window._juceRerouteInProgress, 0, 'flag refcount released after the switch');
});
test('JUCE→HTML5 reroute releases the suppression refcount even if metadata never arrives', async () => {
// If the new HTML5 source never reaches loadedmetadata (bad URL / network
// error), the suppression refcount must still be released — otherwise
// song:play / song:pause stay silenced forever. The backstop timeout (and
// 'error' listener) guarantee release. setTimeout is patched to run
// synchronously here, so the backstop fires immediately.
const sb = makeSandbox({ isAudioRunning: () => false, loadBackingTrack: () => true });
sb.audio.readyState = 0; // metadata not ready → deferred path
sb.audio.addEventListener = () => {}; // loadedmetadata/error never fire
sb.window._juceMode = true;
sb.window._juceAudioUrl = '/audio/song.ogg';
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceRerouteInProgress, 0,
'suppression refcount must not leak when metadata never arrives');
});
test('_rerouteInFlight guard blocks an overlapping invocation past the first await', async () => {
// The flag must be claimed synchronously before isAudioRunning() so a
// second poll tick during a slow IPC cannot run a concurrent switch.
let resolveRunning;
const sb = makeSandbox({
isAudioRunning: () => new Promise((r) => { resolveRunning = r; }),
loadBackingTrack: () => true,
});
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
// First call: parks on the pending isAudioRunning() promise.
const first = sb.window._reevaluateJuceRouting();
// Second call while the first is still awaiting — must early-return.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.__calls.loadBackingTrack.length, 0,
'overlapping invocation must not start a switch while one is in flight');
// Let the first finish.
resolveRunning(true);
await first;
assert.equal(sb.__calls.loadBackingTrack.length, 1, 'the first switch ran exactly once');
});
test('_clearJuceRerouteMemo lets a rejected URL be retried after song teardown', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => false });
sb.window._juceMode = false;
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting(); // hard reject → URL memoised
assert.equal(sb.__calls.loadBackingTrack.length, 1);
// Without a clear, the same URL is skipped.
await sb.window._reevaluateJuceRouting();
assert.equal(sb.__calls.loadBackingTrack.length, 1, 'memoised URL skipped');
// Song teardown clears the memo; a fresh load of the same URL retries.
assert.equal(typeof sb.window._clearJuceRerouteMemo, 'function');
sb.window._clearJuceRerouteMemo();
sb.window._currentSongAudio = { url: '/audio/song.ogg', juceEligible: true };
await sb.window._reevaluateJuceRouting();
assert.equal(sb.__calls.loadBackingTrack.length, 2, 'cleared memo allows a fresh attempt');
});
test('song change mid-flight aborts the switch without mutating routing', async () => {
const sb = makeSandbox({ isAudioRunning: () => true, loadBackingTrack: () => true });
sb.window._juceMode = false;
const original = { url: '/audio/song-a.ogg', juceEligible: true };
sb.window._currentSongAudio = original;
// Swap the current song the moment loadBackingTrack is consulted, so the
// post-await staleness check sees a different _currentSongAudio identity.
sb.window.feedBackDesktop.audio.loadBackingTrack = () => {
sb.window._currentSongAudio = { url: '/audio/song-b.ogg', juceEligible: true };
return Promise.resolve(true);
};
await sb.window._reevaluateJuceRouting();
assert.equal(sb.window._juceMode, false, 'stale switch must not commit JUCE mode');
assert.notEqual(sb.window._juceAudioUrl, '/audio/song-a.ogg', 'stale URL not committed');
});