mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 06:14:29 +00:00
screen.js H/P-section render path at lines 15396-15402 calls _bcLoadSettings()
and _bcFfIdx() (3×) — both moved to bc-panel.js in cut 4 but omitted from the
export list. Browser: first render() after bcCtrl creation → ReferenceError;
seek/loop fast-forward index also dead. Suite was green because no test
executed the butterchurn render path.
Fix:
- export _bcLoadSettings and _bcFfIdx from bc-panel.js
- add both to the tagged import in screen.js
Class-killer (test 12 — generic, not instance-specific):
Extracts all exports from bc-panel.js, all imports in screen.js's
bc-panel.js import clause, then asserts no exported symbol appears as a
bare reference in the screen.js IIFE body without being imported.
Generic: adding a new export + new caller without updating the import → RED.
Own grep (audit):
grep (non-comment lines, all private _bc* names from bc-panel.js):
_bcLoadSettings 1 hit (line 15396)
_bcFfIdx 3 hits (lines 15400-15402)
all others: 0 hits
Only the two symbols Creed found.
Mutation-verify:
remove _bcLoadSettings from screen.js import →
node --test tests/js/highway_3d_bc_panel.test.js
tests 12 pass 11 fail 1 (test 12 RED) ✓
restore →
node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
tests 222 pass 222 fail 0 ✓ GREEN
Plan §8 amended: exports 2→4 with dated note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
240 lines
13 KiB
JavaScript
240 lines
13 KiB
JavaScript
// Class-killer tests for src/bc-panel.js — h3d-carve-4.
|
|
//
|
|
// Most tests are source-scan (grep for structural invariants that protect
|
|
// against specific mutations). Two tests eval _bcIsDesktop in isolation
|
|
// (a pure function that only reads window.*) using new Function so Node
|
|
// can run it without a browser. Screen.js wiring is verified by scanning
|
|
// the import declaration and checking that moved symbols are gone from the
|
|
// IIFE.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const BC_PANEL_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'bc-panel.js');
|
|
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
|
|
|
|
let _src;
|
|
function src() { if (!_src) _src = fs.readFileSync(BC_PANEL_JS, 'utf8'); return _src; }
|
|
|
|
let _screenSrc;
|
|
function screenSrc() { if (!_screenSrc) _screenSrc = fs.readFileSync(SCREEN_JS, 'utf8'); return _screenSrc; }
|
|
|
|
// ── Eval helper for _bcIsDesktop (pure; only reads window.*) ──────────────────
|
|
|
|
let _isDesktopFn;
|
|
function isDesktopFn() {
|
|
if (_isDesktopFn) return _isDesktopFn;
|
|
// Strip 'export' keywords; extract just _bcIsDesktop body for eval.
|
|
const stripped = src().replace(/^export\s+/gm, '');
|
|
// Wrap in a factory that returns _bcIsDesktop after evaluating the whole
|
|
// module (so inner references resolve). window is the only global needed.
|
|
const factory = new Function('window', stripped + '\nreturn _bcIsDesktop;');
|
|
_isDesktopFn = factory;
|
|
return _isDesktopFn;
|
|
}
|
|
|
|
// ── 1. _bcLoading reset on rejection ─────────────────────────────────────────
|
|
|
|
test('_bcLoading reset to null when lib-load promise rejects', () => {
|
|
// Mutation: remove `.catch(() => { _bcLoading = null; })` from _bcLoadLib.
|
|
// Without it, a rejected load-promise is cached forever; every subsequent
|
|
// mount returns the rejected promise immediately → Butterchurn permanently
|
|
// disabled for the session with no retry.
|
|
assert.match(src(), /_bcLoading\s*=\s*null/,
|
|
'_bcLoadLib must reset _bcLoading to null in a .catch handler so failed loads retry');
|
|
// Verify the reset appears in a .catch context (not just an early-return path).
|
|
assert.match(src(), /\.catch\s*\([\s\S]{1,60}_bcLoading\s*=\s*null/,
|
|
'_bcLoading = null must appear inside a .catch callback');
|
|
});
|
|
|
|
// ── 2. window.h3dBcApplySettings at module scope ──────────────────────────────
|
|
|
|
test('window.h3dBcApplySettings is assigned at module scope (not inside a function)', () => {
|
|
// Mutation: move assignment inside _bcCreateController → it isn't available
|
|
// until first mount; settings.html's `?.` call silently no-ops → settings
|
|
// changes (opacity, enabled, cycle mode) never apply until the user visits
|
|
// the player for the first time.
|
|
//
|
|
// Structural check: the assignment must appear BEFORE the first `function`
|
|
// or `export function` declaration in bc-panel.js (i.e. at module scope).
|
|
const s = src();
|
|
// Line-anchored regex: `^window.` matches only an unindented assignment.
|
|
// A comment mention or an indented assignment (inside a function body) both
|
|
// fail to match and return -1 — so this single check is sufficient.
|
|
const assignIdx = s.search(/^window\.h3dBcApplySettings\s*=/m);
|
|
assert.ok(assignIdx >= 0,
|
|
'window.h3dBcApplySettings must be assigned at line-start (module scope) in bc-panel.js — ' +
|
|
'an indented assignment (inside a function) would not match /^window\\./m');
|
|
});
|
|
|
|
// ── 3. _bcIsDesktop guards all three required conditions ──────────────────────
|
|
|
|
test('_bcIsDesktop checks isDesktop, .audio, and typeof getRawAudioFrame', () => {
|
|
// Mutation: remove any one guard → non-desktop host (Docker/web app) enters
|
|
// the desktop guitar-feed path → audioProvider is wrong; pcmLoop errors
|
|
// every 16 ms trying to call an undefined getRawAudioFrame.
|
|
const s = src();
|
|
assert.match(s, /d\.isDesktop/,
|
|
'_bcIsDesktop must guard on d.isDesktop');
|
|
assert.match(s, /d\.audio/,
|
|
'_bcIsDesktop must guard on d.audio');
|
|
assert.match(s, /typeof\s+d\.audio\.getRawAudioFrame\s*===\s*'function'/,
|
|
"_bcIsDesktop must guard typeof d.audio.getRawAudioFrame === 'function'");
|
|
});
|
|
|
|
// ── 4. _bcIsDesktop eval: returns false when window has no desktop bridge ─────
|
|
|
|
test('_bcIsDesktop returns false when window.feedBackDesktop is absent', () => {
|
|
// Mutation: remove the `d && ...` guard → accessing .isDesktop on undefined
|
|
// throws in the browser; the whole highway init crashes.
|
|
const fn = isDesktopFn()({ feedBackDesktop: undefined, slopsmithDesktop: undefined });
|
|
assert.strictEqual(fn(), false,
|
|
'_bcIsDesktop must return false when neither feedBackDesktop nor slopsmithDesktop is set');
|
|
});
|
|
|
|
// ── 5. _bcIsDesktop eval: returns false when isDesktop is missing from bridge ─
|
|
|
|
test('_bcIsDesktop returns false when bridge has audio but no isDesktop flag', () => {
|
|
// Mutation: rely on truthy bridge presence alone (drop isDesktop check) → any host
|
|
// that exposes feedBackDesktop for non-guitar purposes (e.g. file manager) would
|
|
// be misidentified as the guitar-input desktop host.
|
|
const fn = isDesktopFn()({
|
|
feedBackDesktop: { audio: { getRawAudioFrame: () => new Float32Array(512) } },
|
|
slopsmithDesktop: undefined,
|
|
});
|
|
assert.strictEqual(fn(), false,
|
|
'_bcIsDesktop must return false when feedBackDesktop lacks the isDesktop flag');
|
|
});
|
|
|
|
// ── 6. destroy() removes controller from _bcControllers ──────────────────────
|
|
|
|
test('destroy() calls _bcControllers.delete(ctrl)', () => {
|
|
// Mutation: remove delete call → dead controller stays in _bcControllers;
|
|
// _bcApplyAll iterates the Set and calls applySettings() on the dead controller;
|
|
// null canvas/scrim refs throw; multiple repeated mounts eventually saturate Set.
|
|
assert.match(src(), /_bcControllers\.delete\s*\(\s*ctrl\s*\)/,
|
|
'destroy() must call _bcControllers.delete(ctrl) to remove the dead controller');
|
|
});
|
|
|
|
// ── 7. _bcReleaseCanvasGL called in destroy() ─────────────────────────────────
|
|
|
|
test('_bcReleaseCanvasGL is called inside destroy()', () => {
|
|
// Mutation: remove the release call from destroy() → WebGL context not freed on
|
|
// dismount; browsers allow ~16 concurrent contexts; repeated mount/toggles
|
|
// exhaust the cap; subsequent mounts get null from getContext('webgl') →
|
|
// Butterchurn init silently fails.
|
|
//
|
|
// The call appears in two places: destroy() method and the .catch error handler.
|
|
// Both are required. This test checks that destroy() includes it.
|
|
const s = src();
|
|
// `destroy() {` (with space+brace) anchors the actual method definition,
|
|
// not comment references like "so destroy() closes only ...".
|
|
const destroyIdx = s.indexOf('destroy() {');
|
|
assert.ok(destroyIdx >= 0, 'destroy() method must exist in the returned controller object');
|
|
// Find the release call after the destroy label (within 1000 chars).
|
|
const destroyBlock = s.slice(destroyIdx, destroyIdx + 1000);
|
|
assert.match(destroyBlock, /_bcReleaseCanvasGL/,
|
|
'_bcReleaseCanvasGL must be called inside destroy() to free the WebGL context');
|
|
});
|
|
|
|
// ── 8. _bcReleaseCanvasGL called in async-init failure handler ────────────────
|
|
|
|
test('_bcReleaseCanvasGL is called in the _bcLoadLib().catch handler', () => {
|
|
// Mutation: remove from .catch → half-initialised failure (lib load, WebGL
|
|
// context creation) leaves a bound WebGL context on the abandoned canvas;
|
|
// the context is never freed; same cap exhaustion as above.
|
|
const s = src();
|
|
// The error handler's .catch takes a named error param (e) and logs via
|
|
// console.error — that's how we distinguish it from the narrow no-op catches.
|
|
// Look for `_bcReleaseCanvasGL` inside a `.catch((e) => {` block.
|
|
assert.match(s, /\.catch\s*\(\s*\(e\)[\s\S]{1,600}_bcReleaseCanvasGL/,
|
|
'_bcReleaseCanvasGL must appear in the error-handler .catch((e) => {}) block');
|
|
});
|
|
|
|
// ── 9. _bcLoadSettings merges saved state with BC_DEFAULTS ───────────────────
|
|
|
|
test('_bcLoadSettings uses Object.assign with BC_DEFAULTS as base', () => {
|
|
// Mutation: return raw parsed JSON without merging → missing keys from
|
|
// localStorage (fresh install, partial save) become undefined;
|
|
// s.enabled → undefined → bg disabled on first launch.
|
|
assert.match(src(), /Object\.assign\s*\(\s*\{\s*\}\s*,\s*BC_DEFAULTS/,
|
|
'_bcLoadSettings must merge with BC_DEFAULTS so missing keys get defaults');
|
|
});
|
|
|
|
// ── 10. screen.js imports both exports from src/bc-panel.js ──────────────────
|
|
|
|
test('screen.js imports _bcCreateController and _bcIsDesktop from src/bc-panel.js', () => {
|
|
// Mutation: remove import → H-section call to _bcCreateController throws
|
|
// ReferenceError at the first butterchurn mount; the 3D highway becomes
|
|
// permanently broken when butterchurn bg is selected.
|
|
const s = screenSrc();
|
|
assert.match(s,
|
|
/import\s+\{[^}]*_bcCreateController[^}]*\}\s+from\s+['"]\.\/src\/bc-panel\.js['"]/,
|
|
'screen.js must import _bcCreateController from ./src/bc-panel.js');
|
|
assert.match(s,
|
|
/import\s+\{[^}]*_bcIsDesktop[^}]*\}\s+from\s+['"]\.\/src\/bc-panel\.js['"]/,
|
|
'screen.js must import _bcIsDesktop from ./src/bc-panel.js');
|
|
});
|
|
|
|
// ── 12. screen.js has no bare caller references to private bc-panel.js symbols ─
|
|
|
|
test('every bc-panel.js export referenced in screen.js IIFE is in the import statement', () => {
|
|
// Mutation: remove _bcLoadSettings (or any other export) from the screen.js import
|
|
// → symbol is exported by bc-panel.js, referenced in the IIFE, but not bound via
|
|
// import → ReferenceError at runtime. This is the class Creed HIGH found on
|
|
// cut 4 (screen.js:15396-15402: _bcLoadSettings + _bcFfIdx called but not imported).
|
|
//
|
|
// Method (generalised):
|
|
// exported = all _bc* symbols with `export` keyword in bc-panel.js
|
|
// imported = all symbols in screen.js's `from './src/bc-panel.js'` import clause
|
|
// leaked = exported symbols that appear as bare refs in screen.js IIFE
|
|
// but are NOT in imported
|
|
// Adding a new export and a new caller without updating the import → leaked is
|
|
// non-empty → test RED.
|
|
const bcSrc = src();
|
|
const scrSrc = screenSrc();
|
|
|
|
// All exported _bc* symbols from bc-panel.js.
|
|
const exported = new Set(
|
|
[...bcSrc.matchAll(/^export\s+(?:const|let|var|function)\s+(_bc\w+)/mg)].map(m => m[1])
|
|
);
|
|
|
|
// Symbols actually imported from bc-panel.js in screen.js.
|
|
const importMatch = scrSrc.match(/import\s+\{([^}]+)\}\s+from\s+['"]\.\/src\/bc-panel\.js['"]/);
|
|
const imported = new Set(
|
|
importMatch ? importMatch[1].split(',').map(s => s.trim()).filter(Boolean) : []
|
|
);
|
|
|
|
// IIFE body: strip import lines and line comments to avoid false positives.
|
|
const noImports = scrSrc.replace(/^import\s+.*\n/gm, '');
|
|
const noComments = noImports.replace(/\/\/[^\n]*/g, '');
|
|
|
|
// Exported symbols referenced in the IIFE body but absent from the import list.
|
|
const leaked = [...exported].filter(
|
|
sym => new RegExp('\\b' + sym + '\\b').test(noComments) && !imported.has(sym)
|
|
);
|
|
|
|
assert.deepStrictEqual(leaked, [],
|
|
'screen.js references bc-panel.js exports that are not in its import clause: ' +
|
|
leaked.join(', ') +
|
|
' — add them to the import { … } from \'./src/bc-panel.js\' line in screen.js');
|
|
});
|
|
|
|
// ── 11. screen.js IIFE no longer defines moved B-section symbols ──────────────
|
|
|
|
test('screen.js IIFE does not redeclare _bcCreateController or _bcLoadLib', () => {
|
|
// Mutation: re-add function _bcCreateController() to the IIFE → double definition;
|
|
// the IIFE-scope function shadows the imported one inside the factory; bc-panel.js
|
|
// private state is split: the IIFE copy has its own _bcControllers, _bcSettings, etc.
|
|
// The panel never shows, controller objects leak, applySettings no-ops.
|
|
const s = screenSrc();
|
|
// Strip import lines so we only scan the IIFE body.
|
|
const iife = s.replace(/^import\s+.*\n/gm, '');
|
|
assert.doesNotMatch(iife, /function\s+_bcCreateController\s*\(/,
|
|
'IIFE must not redeclare _bcCreateController');
|
|
assert.doesNotMatch(iife, /function\s+_bcLoadLib\s*\(/,
|
|
'IIFE must not redeclare _bcLoadLib');
|
|
});
|