Files
feedBack/tests/js/highway_3d_camera_bootstrap.test.js
T
byrongamatosandClaude Sonnet 4.6 7623ad85e3 feat(h3d-carve-15): extract U-section (per-frame renderer) into src/renderer.js
- createRenderer factory DI: 241 params (consts, fn-refs, 33 pool getters,
  material/settings/camera/ND getters, 35 setters)
- screen.js: tombstone + createRenderer wiring after createNoteRenderer +
  createCamera wirings (§1 ruling: createNoteRenderer called from screen.js)
- Restore createArp wiring + _resetStringDependentCaches to screen.js
  (accidentally dropped during carve; both needed in IIFE scope)
- smoothNow: correction 3 — setter form setFrameNow(v); return v (no bare
  return (_frameNow = raw))
- _applyNoteCamTargets callers: 2 sites (7940/9923); correction 2 verified
- lookaheadSmoothCamStep callers: 3 sites (9963/9974/9978); correction 2 verified
- plugin.json: bump 3.50.0 → 3.51.0

Tests (16 files updated to scan renderer.js):
- highway_3d_renderer.test.js: new, 15 tests — export contract, DI count
  (241), tombstone, caller-list corrections, smoothNow semantics, ordering
- highway_3d_arp_deferral.test.js: add renderer.js scan (deferChordGems /
  noteStreamCoversArpShape moved to renderer.js)
- highway_3d_lean_sustain.test.js: add renderer.js scan; update to
  setLeanSus/getLeanSus() getter form
- highway_3d_smooth_clock_pause.test.js: fix literal-newline syntax error;
  update to setClkAudioT/setClkPerf/setFrameNow setter form;
  update new-sample regex to match getClkAudioT()
- highway_3d_slide_target.test.js: add renderer.js to src scan
- highway_3d_sustain_rail.test.js: assert against rendererSrc (pattern
  moved from U-section)
- highway_chart_transform.test.js: add utils.js scan (_openStringPitchLabels-
  ForTuning moved to src/utils.js by h3d-carve-3)
- highway_note_state.test.js: add renderer.js scan (_ndGetNoteState /
  _ndHasProvider captures in renderer.js update())
- highway_3d_camera_bootstrap.test.js: setter form for camSnapped/curX/
  measureStarts; renderer.js added to scan
- highway_3d_camera_framing.test.js: renderer.js added; setMeasureStarts/
  setCamSnapped setter form in assertions

Suite: 1395/1396 (test 46 pre-existing failure unrelated to carve-15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 22:47:48 +02:00

364 lines
15 KiB
JavaScript

// Regression coverage for the first-chart-data camera bootstrap in
// plugins/highway_3d/screen.js.
//
// The event selector is pure and tested behaviourally. The renderer lifecycle
// wiring remains source-level, matching the existing highway_3d camera tests:
// constructing a full Three.js renderer in Node would test a large fake DOM/GL
// harness rather than the bootstrap contract itself.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const SCREEN_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js');
const RENDERER_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'renderer.js');
// h3d-carve-15: U-section (bootstrap region C) moved to renderer.js; scan both.
const src = fs.readFileSync(SCREEN_JS, 'utf8') + '\n' + fs.readFileSync(RENDERER_JS, 'utf8');
// Since h3d-carve-1b, hwyFirstRelevantFrettedTime lives in geometry.js.
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
const geoSrc = fs.readFileSync(GEOMETRY_JS, 'utf8');
// h3d-carve-9: camUpdate body moved to camera.js — extractFn retargets there.
const CAMERA_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'camera.js');
const cameraSrc = fs.readFileSync(CAMERA_JS, 'utf8');
function extractFn(source, name) {
const start = source.indexOf('function ' + name);
assert.ok(start >= 0, `function ${name} must exist`);
const open = source.indexOf('{', start);
let depth = 0;
for (let i = open; i < source.length; i++) {
if (source[i] === '{') depth++;
else if (source[i] === '}' && --depth === 0) return source.slice(start, i + 1);
}
throw new Error(`unbalanced braces extracting ${name}`);
}
function sourceBetween(startText, endText) {
const start = src.indexOf(startText);
assert.ok(start >= 0, `missing source anchor: ${startText}`);
const end = src.indexOf(endText, start);
assert.ok(end > start, `missing source end anchor: ${endText}`);
return src.slice(start, end);
}
const hwyFirstRelevantFrettedTime = new Function(
'"use strict";'
+ extractFn(geoSrc, 'hwyFirstRelevantFrettedTime')
+ '\nreturn hwyFirstRelevantFrettedTime;',
)();
test('long intros bootstrap from the earliest future fretted note', () => {
const notes = [
{ t: 13.22, s: 2, f: 7 },
{ t: 15.0, s: 1, f: 4 },
];
const chords = [
{ t: 14.0, notes: [{ s: 0, f: 3 }, { s: 1, f: 5 }] },
];
assert.equal(hwyFirstRelevantFrettedTime(notes, chords, 0.4, 0.2, 6), 13.22);
});
test('chord-only charts bootstrap from fretted chord members', () => {
const chords = [
{ t: 4.0, notes: [{ s: 0, f: 0 }, { s: 1, f: 0 }] },
{ t: 8.5, notes: [{ s: 0, f: 0 }, { s: 1, f: 9 }] },
];
assert.equal(hwyFirstRelevantFrettedTime([], chords, 0, 0.2, 6), 8.5);
});
test('empty and all-open charts keep the default camera', () => {
assert.equal(hwyFirstRelevantFrettedTime([], [], 0, 0.2, 6), null);
assert.equal(hwyFirstRelevantFrettedTime(
[{ t: 2, s: 0, f: 0 }],
[{ t: 3, notes: [{ s: 1, f: 0 }, { s: 2, f: 0 }] }],
0,
0.2,
6,
), null);
});
test('bootstrap ignores malformed strings but supports extended-range charts', () => {
const notes = [
{ t: 1, s: -1, f: 4 },
{ t: 2, s: 7, f: 5 },
{ t: 3, s: 6, f: 8 },
];
assert.equal(hwyFirstRelevantFrettedTime(notes, [], 0, 0.2, 6), null);
assert.equal(hwyFirstRelevantFrettedTime(notes, [], 0, 0.2, 7), 3);
});
test('active sustains bootstrap at now and fully expired events are skipped', () => {
const now = 10;
assert.equal(hwyFirstRelevantFrettedTime(
[{ t: 6, sus: 5, s: 2, f: 7 }],
[],
now,
0.2,
6,
), now);
assert.equal(hwyFirstRelevantFrettedTime(
[{ t: 6, sus: 1, s: 2, f: 7 }, { t: 15, s: 2, f: 9 }],
[],
now,
0.2,
6,
), 15);
});
test('recent onsets inside the behind-window bootstrap at now', () => {
assert.equal(hwyFirstRelevantFrettedTime(
[{ t: 9.9, s: 2, f: 7 }],
[],
10,
0.2,
6,
), 10);
});
test('bootstrap runs once when complete chart arrays arrive', () => {
const bootstrap = sourceBetween(
'// ── Camera bootstrap (first chart data)',
' pbBeg(4);',
);
assert.match(
bootstrap,
/if\s*\(\s*!getCamSnapped\s*\(\s*\)\s*&&\s*!getCamPreScanned\s*\(\s*\)\s*&&\s*notes\s*&&\s*chords\s*\)/,
'chart bootstrap must be gated to one pass after both arrays arrive',
);
assert.match(
bootstrap,
/hwyFirstRelevantFrettedTime\(\s*notes\s*,\s*chords\s*,\s*now\s*,\s*CAM_TGT_BEHIND\s*,\s*nStr\s*\)/,
'bootstrap must select the first relevant event using the active string count',
);
assert.match(
bootstrap,
/firstFrettedTime\s*===\s*null[\s\S]*?setCamSnapped\s*\(\s*true\s*\)/,
'all-open/empty charts without lookahead bounds must permanently disable bootstrap work',
);
});
test('steady and lookahead modes initialize immediately from future chart data', () => {
const bootstrap = sourceBetween(
'// ── Camera bootstrap (first chart data)',
' pbBeg(4);',
);
assert.match(
bootstrap,
/cameraMode\s*===\s*'lookahead'[\s\S]*?lookaheadBoundsNow\s*\|\|\s*firstFrettedTime\s*!==\s*null/,
'lookahead anchor bounds must bootstrap even on an all-open chart',
);
assert.match(
bootstrap,
/lookaheadBootstrapTime\(\s*now\s*,\s*firstFrettedTime\s*\)/,
'lookahead mode must project to the first window that reaches the phrase',
);
assert.match(
bootstrap,
/lookaheadBoundsNow\s*\?\s*now\s*:\s*lookaheadBootstrapTime/,
'already-live anchor/note bounds must win over a projected lookahead',
);
assert.match(
bootstrap,
/Math\.max\(\s*now\s*,\s*firstFrettedTime\s*-\s*camAhead\s*\)/,
'steady mode must sample when the first event enters its normal target window',
);
assert.match(
bootstrap,
/setCurX\s*\(\s*getTgtX\s*\(\)\s*\)\s*;[\s\S]*?setCurDist\s*\(\s*getTgtDist\s*\(\)\s*\)\s*;/,
'the initial base position must be applied before the note draw loop',
);
});
test('silent-intro hold hands off only when live framing is ready', () => {
const target = sourceBetween(
'// ── Camera target',
'// ── Chord diagram:',
);
assert.match(
target,
/cameraMode\s*===\s*'lookahead'\s*\?\s*lookaheadBoundsNow\s*!==\s*null\s*:\s*camDistGot/,
'lookahead and steady modes must use their own live-ready signal',
);
assert.match(
target,
/if\s*\(\s*bootstrapHoldActive\s*\)[\s\S]*?lockActive\s*=\s*getPrevLockActive\s*\(\s*\)/,
'the bootstrap target must remain untouched while the live window is empty',
);
assert.match(
target,
// h3d-carve-15: bare assignments → setter calls in renderer.js
/getCamBootstrapMode\(\)\s*!==\s*cameraMode[\s\S]*?setCamBootstrapHolding\s*\(\s*false\s*\)/,
'a live camera-mode change must safely release the old-mode hold',
);
});
test('song changes and teardown reset every bootstrap state field', () => {
// h3d-carve-15: song-change path uses setter calls (renderer.js);
// teardown/init path uses bare assignments (screen.js). Both must exist.
const setterResets = src.match(
/setCamSnapped\s*\(\s*false\s*\)\s*;\s*\r?\n\s*setCamPreScanned\s*\(\s*false\s*\)/g,
) || [];
const bareResets = src.match(
/_camSnapped\s*=\s*false\s*;\s*\r?\n\s*_camPreScanned\s*=\s*false/g,
) || [];
const totalResets = setterResets.length + bareResets.length;
assert.equal(
totalResets,
2,
`song-change and teardown paths must both reset bootstrap state (setter=${setterResets.length}, bare=${bareResets.length})`,
);
});
test('Camera Director still layers after the bootstrapped auto-framing base', () => {
const bootstrap = sourceBetween(
'// ── Camera bootstrap (first chart data)',
' pbBeg(4);',
);
assert.doesNotMatch(
bootstrap,
/_freeCam|__h3dCamCtl/,
'bootstrap must only initialize base framing, never mutate Camera Director state',
);
// h3d-carve-9: extractFn must target cameraSrc — src holds only the tombstone.
// tgtX is DI-rewired to getTgtX() direct call in camera.js.
const camUpdate = extractFn(cameraSrc, 'camUpdate');
const baseIndex = camUpdate.indexOf('curX += (getTgtX() - curX) * lerp');
const directorIndex = camUpdate.indexOf('if (_freeCam && _freeCam.enabled)');
const positionIndex = camUpdate.indexOf('cam.position.set(_camX, _camY, _camZ)');
assert.ok(
baseIndex >= 0 && directorIndex > baseIndex && positionIndex > directorIndex,
'Camera Director transforms must remain layered after base framing and before camera placement',
);
});
// ── h3d-carve-9: setter class-killers (write-back pairs must survive DI) ────
// Severing the call turns the test RED: a silent local var replaces the
// write-back and the IIFE-scope var is never updated.
test('setCurX write-back is called in camUpdate (curX persists across frames)', () => {
// Silencing: sed 's/setCurX(curX)/\/\/ GUTTED/' → this test fails.
assert.match(
cameraSrc,
/setCurX\(\s*curX\s*\)/,
'camUpdate must write curX back via setCurX(); removing it silences the update',
);
});
test('setFretRowFitBoost write-back is called in camUpdate (boost persists across frames)', () => {
// Silencing: sed 's/setFretRowFitBoost(_fretRowFitBoost)/\/\/ GUTTED/' → RED.
assert.match(
cameraSrc,
/setFretRowFitBoost\(\s*_fretRowFitBoost\s*\)/,
'camUpdate must write _fretRowFitBoost back via setFretRowFitBoost(); removing it silences the boost',
);
});
// ── h3d-carve-9 Creed r1: naming-correspondence guard (param-swap class-killer) ──
// Structural source-scan: every entry in createCamera({...}) must satisfy its
// naming-correspondence class. Kills swaps like (CAM_H_BASE: CAM_DIST_BASE) and
// wrong-var getters ((getCurX: () => curDist)) across the whole wiring surface.
// cut-13 additions inherit the guard automatically; only the pinned fn-ref renames
// need a one-line entry in PINNED_RENAMES when a new rename is introduced.
test('createCamera({...}) wiring has correct naming correspondence (no param swaps)', () => {
// Fn-ref renames that intentionally differ from shorthand — pinned exhaustively.
const PINNED_RENAMES = {
freeCamFor: '_freeCamFor',
aspectPaneKey: '_aspectPaneKey',
resolveTuneFor: '_resolveTuneFor',
aspectRegisterPane: '_aspectRegisterPane',
};
// 1. Extract the argument block from the createCamera call.
// h3d-carve-13: destructure expanded with lookahead exports — update anchor string.
const ANCHOR = 'const { effectiveVfov, camUpdate, lookaheadBootstrapTime, lookaheadComputeFretBounds, lookaheadTargetWorldX } = createCamera({';
const callStart = src.indexOf(ANCHOR);
assert.ok(callStart >= 0, 'createCamera call must be findable in screen.js');
const blockStart = callStart + ANCHOR.length - 1; // points to the opening {
assert.equal(src[blockStart], '{', 'expected { at computed blockStart');
let depth = 0, blockEnd = -1;
for (let i = blockStart; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}' && --depth === 0) { blockEnd = i; break; }
}
assert.ok(blockEnd > blockStart, 'createCamera argument block must have balanced braces');
const inner = src.slice(blockStart + 1, blockEnd);
// 2. Split into entries at depth-0 commas (setter bodies contain { } — skip them).
const rawEntries = [];
let current = '', d = 0;
for (let i = 0; i < inner.length; i++) {
const ch = inner[i];
if (ch === '{') d++;
else if (ch === '}') d--;
if (ch === ',' && d === 0) {
const t = current.trim();
if (t) rawEntries.push(t);
current = '';
} else {
current += ch;
}
}
if (current.trim()) rawEntries.push(current.trim());
// Strip line comments and blank entries.
const entries = rawEntries
.map(e => e.replace(/\/\/[^\n]*/g, '').trim())
.filter(Boolean);
assert.ok(entries.length >= 48, `expected at least 48 entries, got ${entries.length}`);
const violations = [];
for (const entry of entries) {
if (!entry.includes(':')) {
// Shorthand — key === value by definition (BASE_VFOV, sY, …).
continue;
}
const colonIdx = entry.indexOf(':');
const key = entry.slice(0, colonIdx).trim();
const value = entry.slice(colonIdx + 1).trim();
if (key in PINNED_RENAMES) {
if (value !== PINNED_RENAMES[key]) {
violations.push(`${key}: pinned to '${PINNED_RENAMES[key]}' but got '${value}'`);
}
continue;
}
if (key.startsWith('get')) {
// () => [_]varStem — varStem (no underscore) must match key minus 'get' prefix.
const expectedStem = key[3].toLowerCase() + key.slice(4);
const m = value.match(/^\(\)\s*=>\s*_?(\w+)$/);
if (!m) {
violations.push(`${key}: getter value '${value}' does not match () => [_]var`);
continue;
}
if (m[1] !== expectedStem) {
violations.push(`${key}: getter body references var stem '${m[1]}' but expected '${expectedStem}'`);
}
continue;
}
if (key.startsWith('set')) {
// (v) => { [_]varStem = v; } — varStem must match key minus 'set' prefix.
const expectedStem = key[3].toLowerCase() + key.slice(4);
const m = value.match(/^\(v\)\s*=>\s*\{\s*_?(\w+)\s*=\s*v\s*;\s*\}$/);
if (!m) {
violations.push(`${key}: setter value '${value}' does not match (v) => { [_]var = v; }`);
continue;
}
if (m[1] !== expectedStem) {
violations.push(`${key}: setter assigns var stem '${m[1]}' but expected '${expectedStem}'`);
}
continue;
}
// key:value form that is NOT a getter, setter, or pinned rename — disallowed.
violations.push(`${key}: key:value entry not in PINNED_RENAMES and not a get/set arrow`);
}
assert.deepEqual(violations, [], `createCamera wiring violations found`);
});