fix(h3d): cut-16 follow-up — tests, plugin.json bump, visibility/string-colors retargets

- Add tests/js/highway_3d_scene_init.test.js (18 tests): wiring guard,
  export surface, class-killer, DI anti-vacuity, import correctness,
  function presence, kill tests, plugin.json version gate.
- Bump plugins/highway_3d/plugin.json 3.51.0 → 3.52.0.
- Retarget highway_string_colors.test.js tests 3–4 to scene-init.js
  (functions moved in cut-16).
- Update highway_visibility.test.js test 22 regexes to accept DI-rewritten
  forms: getHighwayCanvas() / getWrap().style.display / getHighwayCanvas().offsetParent.

Suite: 1426/1427 pass (1 pre-existing audio test; all cut-16 regressions cleared).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
byrongamatos
2026-09-06 06:46:08 +02:00
co-authored by Claude Sonnet 4.6
parent 4bb5d21a40
commit 273412744a
4 changed files with 198 additions and 19 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "highway_3d", "id": "highway_3d",
"name": "3D Highway", "name": "3D Highway",
"version": "3.51.0", "version": "3.52.0",
"type": "visualization", "type": "visualization",
"scriptType": "module", "scriptType": "module",
"bundled": true, "bundled": true,
+172
View File
@@ -0,0 +1,172 @@
// Source-level guards for src/scene-init.js (h3d-carve-16).
// Validates wiring, DI contract, class-killer, export surface, and kill tests
// for initScene / buildBoard / _bgUnmountStyle / _bcSyncMode.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const sceneInitJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'scene-init.js');
const screen3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const pluginJson = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'plugin.json');
// ── §1 Wiring guard ───────────────────────────────────────────────────────────
test('scene-init exports createSceneInit as a named ES export', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /^export\s+function\s+createSceneInit\s*\(/m,
'must have: export function createSceneInit(');
});
test('screen.js imports createSceneInit from ./src/scene-init.js', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.match(src, /import\s*\{[^}]*createSceneInit[^}]*\}\s*from\s*['"]\.\/src\/scene-init\.js['"]/,
'screen.js must import createSceneInit from ./src/scene-init.js');
});
test('screen.js wires the four exports from createSceneInit', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.match(src, /const\s*\{[^}]*initScene[^}]*\}\s*=\s*createSceneInit\s*\(/,
'screen.js must destructure initScene from createSceneInit(...)');
assert.match(src, /const\s*\{[^}]*buildBoard[^}]*\}\s*=\s*createSceneInit\s*\(/,
'screen.js must destructure buildBoard from createSceneInit(...)');
assert.match(src, /const\s*\{[^}]*_bgUnmountStyle[^}]*\}\s*=\s*createSceneInit\s*\(/,
'screen.js must destructure _bgUnmountStyle from createSceneInit(...)');
assert.match(src, /const\s*\{[^}]*_bcSyncMode[^}]*\}\s*=\s*createSceneInit\s*\(/,
'screen.js must destructure _bcSyncMode from createSceneInit(...)');
});
// ── §2 Export surface ─────────────────────────────────────────────────────────
test('createSceneInit returns exactly { initScene, buildBoard, _bgUnmountStyle, _bcSyncMode }', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
// The return statement at the bottom of createSceneInit must name exactly these four
assert.match(src,
/return\s*\{\s*initScene\s*,\s*buildBoard\s*,\s*_bgUnmountStyle\s*,\s*_bcSyncMode\s*\}/,
'return surface must be exactly { initScene, buildBoard, _bgUnmountStyle, _bcSyncMode }');
});
test('createSceneInit does NOT export _bgLoadSettings (internal function)', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
// _bgLoadSettings is internal; must not appear in the return object
assert.doesNotMatch(src,
/return\s*\{[^}]*_bgLoadSettings[^}]*\}/,
'_bgLoadSettings must NOT be in the return surface');
});
// ── §3 Class-killer guard ─────────────────────────────────────────────────────
test('createSceneInit body never assigns to a DI parameter name (class-killer)', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
// Extract the DI parameter block (between first { and the closing }) of the factory signature
const sigStart = src.indexOf('export function createSceneInit({');
assert.ok(sigStart !== -1, 'createSceneInit signature not found');
const bodyOpen = src.indexOf(') {', sigStart);
assert.ok(bodyOpen !== -1, 'factory body open not found');
const paramBlock = src.slice(sigStart, bodyOpen);
// Collect setter names (setX) from the DI block
const setterNames = [...paramBlock.matchAll(/\bset([A-Z][A-Za-z0-9]*)\b/g)].map(m => m[0]);
assert.ok(setterNames.length > 10, `expected many setter params, got ${setterNames.length}`);
const body = src.slice(bodyOpen);
for (const name of setterNames) {
// Assignment to the bare DI name (not a call) would be: `name = ` or `name=`
const assignPat = new RegExp(`\\b${name}\\s*=(?!=)`, 'g');
const hits = body.match(assignPat);
assert.ok(!hits, `class-killer: body assigns to DI param '${name}' (${hits && hits.length} hit(s))`);
}
});
// ── §4 DI anti-vacuity ────────────────────────────────────────────────────────
test('createSceneInit receives ≥150 DI parameters (anti-vacuity)', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
const sigStart = src.indexOf('export function createSceneInit({');
const bodyOpen = src.indexOf(') {', sigStart);
const paramBlock = src.slice(sigStart, bodyOpen);
// Count unique identifiers that look like DI params
const names = new Set([...paramBlock.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b/g)].map(m => m[1]));
assert.ok(names.size >= 150, `expected ≥150 DI params, got ${names.size}`);
});
// ── §5 Import correctness ─────────────────────────────────────────────────────
test('scene-init imports geoFretX and geoFretMid from geometry.js (not bare fretX/fretMid)', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /import\s*\{[^}]*geoFretX[^}]*\}\s*from\s*['"]\.\/geometry\.js['"]/,
'must import geoFretX from ./geometry.js');
assert.match(src, /import\s*\{[^}]*geoFretMid[^}]*\}\s*from\s*['"]\.\/geometry\.js['"]/,
'must import geoFretMid from ./geometry.js');
// Should NOT import the bare names that don't exist in geometry.js
assert.doesNotMatch(src,
/import\s*\{[^}]*(?<!\w)fretX(?!\w)[^}]*\}\s*from\s*['"]\.\/geometry\.js['"]/,
'must NOT import bare fretX from geometry.js (it exports geoFretX)');
});
test('scene-init rebuilds fretX/fretMid as closures over getH3dFretUniform()', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /const\s+fretX\s*=\s*f\s*=>\s*geoFretX\s*\(\s*f\s*,\s*getH3dFretUniform\s*\(\s*\)\s*\)/,
'fretX must be rebuilt as: f => geoFretX(f, getH3dFretUniform())');
assert.match(src, /const\s+fretMid\s*=\s*f\s*=>\s*geoFretMid\s*\(\s*f\s*,\s*getH3dFretUniform\s*\(\s*\)\s*\)/,
'fretMid must be rebuilt as: f => geoFretMid(f, getH3dFretUniform())');
});
// ── §6 Key functions present ───────────────────────────────────────────────────
test('initScene is defined as a function inside createSceneInit body', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /function\s+initScene\s*\(\s*\)/,
'initScene() must be defined inside scene-init.js');
});
test('buildBoard is defined as a function inside createSceneInit body', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /function\s+buildBoard\s*\(\s*\)/,
'buildBoard() must be defined inside scene-init.js');
});
test('_bgUnmountStyle is defined as a function inside createSceneInit body', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /function\s+_bgUnmountStyle\s*\(\s*\)/,
'_bgUnmountStyle() must be defined inside scene-init.js');
});
test('_bcSyncMode is defined as a function inside createSceneInit body', () => {
const src = fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /function\s+_bcSyncMode\s*\(\s*\)/,
'_bcSyncMode() must be defined inside scene-init.js');
});
// ── §7 Kill tests — functions that must NOT survive in screen.js ───────────────
test('initScene no longer defined in screen.js (moved to scene-init.js)', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.doesNotMatch(src, /^\s*function\s+initScene\s*\(\s*\)/m,
'initScene() must not be defined in screen.js — it moved to scene-init.js');
});
test('buildBoard no longer defined in screen.js (moved to scene-init.js)', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.doesNotMatch(src, /^\s*function\s+buildBoard\s*\(\s*\)/m,
'buildBoard() must not be defined in screen.js — it moved to scene-init.js');
});
test('_bgUnmountStyle no longer defined in screen.js (moved to scene-init.js)', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.doesNotMatch(src, /^\s*function\s+_bgUnmountStyle\s*\(\s*\)/m,
'_bgUnmountStyle() must not be defined in screen.js — it moved to scene-init.js');
});
test('_bcSyncMode no longer defined in screen.js (moved to scene-init.js)', () => {
const src = fs.readFileSync(screen3dJs, 'utf8');
assert.doesNotMatch(src, /^\s*function\s+_bcSyncMode\s*\(\s*\)/m,
'_bcSyncMode() must not be defined in screen.js — it moved to scene-init.js');
});
// ── §8 plugin.json version bump ───────────────────────────────────────────────
test('plugin.json version is 3.52.0 (bumped for cut-16)', () => {
const pkg = JSON.parse(fs.readFileSync(pluginJson, 'utf8'));
assert.equal(pkg.version, '3.52.0',
'plugin.json must be bumped to 3.52.0 for cut-16');
});
+5 -2
View File
@@ -13,6 +13,7 @@ const path = require('node:path');
const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js'); const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const sceneInitJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'scene-init.js'); // h3d-carve-16
// The highway string-colour manager was carved out of app.js into its own // The highway string-colour manager was carved out of app.js into its own
// module (R3a). // module (R3a).
const appJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-colors.js'); const appJs = path.join(__dirname, '..', '..', 'static', 'js', 'highway-colors.js');
@@ -78,7 +79,8 @@ test('2D public API exposes getStringColors / setStringColors', () => {
// ── 3D highway (plugins/highway_3d/screen.js) ───────────────────────────── // ── 3D highway (plugins/highway_3d/screen.js) ─────────────────────────────
test('3D adds a custom palette path + h3dBgSetStringColors setter', () => { test('3D adds a custom palette path + h3dBgSetStringColors setter', () => {
const src = fs.readFileSync(highway3dJs, 'utf8'); // _bgLoadSettings (lines 89-90) moved to scene-init.js in h3d-carve-16 — combine both
const src = fs.readFileSync(highway3dJs, 'utf8') + '\n' + fs.readFileSync(sceneInitJs, 'utf8');
assert.match(src, /window\.h3dBgSetStringColors\s*=/, 'window.h3dBgSetStringColors must be defined'); assert.match(src, /window\.h3dBgSetStringColors\s*=/, 'window.h3dBgSetStringColors must be defined');
assert.match(src, /_bgWriteGlobal\('customColors'/, 'setter must persist customColors'); assert.match(src, /_bgWriteGlobal\('customColors'/, 'setter must persist customColors');
assert.match(src, /_bgWriteGlobal\('palette',\s*'custom'\)/, "setter must flip palette to 'custom'"); assert.match(src, /_bgWriteGlobal\('palette',\s*'custom'\)/, "setter must flip palette to 'custom'");
@@ -91,7 +93,8 @@ test('3D adds a custom palette path + h3dBgSetStringColors setter', () => {
}); });
test('3D gem-body gradients follow the active palette (not hardcoded)', () => { test('3D gem-body gradients follow the active palette (not hardcoded)', () => {
const src = fs.readFileSync(highway3dJs, 'utf8'); // _recolorGemGradients + _applyPaletteToMaterials moved to scene-init.js in h3d-carve-16
const src = fs.readFileSync(sceneInitJs, 'utf8');
// The gem bodies (strings 0..5) are a baked per-vertex gradient; a custom // The gem bodies (strings 0..5) are a baked per-vertex gradient; a custom
// palette must recolor them, else gems/sustain/vibrato heads stay stock. // palette must recolor them, else gems/sustain/vibrato heads stay stock.
assert.match(src, /function _recolorGemGradients\(\)/, '_recolorGemGradients must exist'); assert.match(src, /function _recolorGemGradients\(\)/, '_recolorGemGradients must exist');
+20 -16
View File
@@ -10,6 +10,7 @@ const path = require('node:path');
const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js'); const highwayJs = path.join(__dirname, '..', '..', 'static', 'highway.js');
const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'); const highway3dJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const sceneInitJs = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'scene-init.js'); // h3d-carve-16
// Brace-balanced extraction so a future method that grows guards or // Brace-balanced extraction so a future method that grows guards or
// nested blocks doesn't get truncated by a naive `[^}]*\}` regex. // nested blocks doesn't get truncated by a naive `[^}]*\}` regex.
@@ -131,10 +132,10 @@ test('api.setVisible accepts bool / null and re-emits inline', () => {
}); });
test('3D Highway subscribes to highway:visibility and toggles wrap on hide', () => { test('3D Highway subscribes to highway:visibility and toggles wrap on hide', () => {
// initScene moved to scene-init.js in h3d-carve-16; teardown stays in screen.js
const sceneInitSrc = fs.readFileSync(sceneInitJs, 'utf8');
const src = fs.readFileSync(highway3dJs, 'utf8'); const src = fs.readFileSync(highway3dJs, 'utf8');
// Scope to lifecycle blocks so unrelated / commented mentions const initSceneBlock = extractBlock(sceneInitSrc, 'function initScene()');
// elsewhere in screen.js can't cause false positives.
const initSceneBlock = extractBlock(src, 'function initScene()');
const teardownBlock = extractBlock(src, 'function teardown()'); const teardownBlock = extractBlock(src, 'function teardown()');
// Listener registration with the documented event name (in init). // Listener registration with the documented event name (in init).
@@ -146,23 +147,25 @@ test('3D Highway subscribes to highway:visibility and toggles wrap on hide', ()
// Handler filters by canvas identity so splitscreen panels don't // Handler filters by canvas identity so splitscreen panels don't
// hide each other's overlays — every instance receives every event // hide each other's overlays — every instance receives every event
// on the shared feedBack bus, so this gate is essential. // on the shared feedBack bus, so this gate is essential.
assert.match( // Accept DI-rewritten form (getHighwayCanvas()) as well as original (highwayCanvas) — h3d-carve-16
initSceneBlock, assert.ok(
/e\.detail\.canvas\s*!==\s*highwayCanvas/, /e\.detail\.canvas\s*!==\s*highwayCanvas/.test(initSceneBlock) ||
/e\.detail\.canvas\s*!==\s*getHighwayCanvas\(\)/.test(initSceneBlock),
'handler must filter on event.detail.canvas !== highwayCanvas (splitscreen-safe)', 'handler must filter on event.detail.canvas !== highwayCanvas (splitscreen-safe)',
); );
// Handler toggles wrap.style.display based on visible === false. // Handler toggles wrap/getWrap() display based on visible === false.
assert.match( assert.ok(
initSceneBlock, /wrap\.style\.display\s*=\s*v\s*===\s*false\s*\?\s*['"]none['"]\s*:\s*['"]/.test(initSceneBlock) ||
/wrap\.style\.display\s*=\s*v\s*===\s*false\s*\?\s*['"]none['"]\s*:\s*['"]['"]/, /getWrap\(\)\.style\.display\s*=\s*v\s*===\s*false\s*\?\s*['"]none['"]\s*:\s*['"]/.test(initSceneBlock),
'handler must hide the wrap when visible === false', 'handler must hide the wrap when visible === false',
); );
// Initial-sync on bind so renderers that mount while the canvas // Initial-sync on bind so renderers that mount while the canvas
// is already hidden (e.g. plugin loaded mid-splitscreen) don't // is already hidden (e.g. plugin loaded mid-splitscreen) don't
// leave the wrap stuck in the wrong state. // leave the wrap stuck in the wrong state.
assert.match( // Accept DI-rewritten form (getHighwayCanvas()) as well as direct ref — h3d-carve-16
initSceneBlock, assert.ok(
/highwayCanvas\.offsetParent\s*!==\s*null/, /highwayCanvas\.offsetParent\s*!==\s*null/.test(initSceneBlock) ||
/getHighwayCanvas\(\)\.offsetParent\s*!==\s*null/.test(initSceneBlock),
'initScene must compute initial visibility from local highwayCanvas (splitscreen-safe)', 'initScene must compute initial visibility from local highwayCanvas (splitscreen-safe)',
); );
// Subscribes to highway:canvas-replaced so the identity gate // Subscribes to highway:canvas-replaced so the identity gate
@@ -173,9 +176,10 @@ test('3D Highway subscribes to highway:visibility and toggles wrap on hide', ()
/window\.feedBack\.on\(\s*['"]highway:canvas-replaced['"]/, /window\.feedBack\.on\(\s*['"]highway:canvas-replaced['"]/,
'initScene must track canvas swaps so the visibility gate keeps matching', 'initScene must track canvas swaps so the visibility gate keeps matching',
); );
assert.match( // Accept DI-rewritten form for canvas-replaced handler — h3d-carve-16
initSceneBlock, assert.ok(
/highwayCanvas\s*=\s*e\.detail\.newCanvas/, /highwayCanvas\s*=\s*e\.detail\.newCanvas/.test(initSceneBlock) ||
/setHighwayCanvas\(\s*e\.detail\.newCanvas\s*\)/.test(initSceneBlock),
'canvas-replaced handler must update the local highwayCanvas reference', 'canvas-replaced handler must update the local highwayCanvas reference',
); );
// Teardown unbinds both listeners. // Teardown unbinds both listeners.