mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 04:41:23 +00:00
* 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>
343 lines
15 KiB
JavaScript
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');
|
|
});
|