Files
feedBack/tests/js/highway_3d_geometry.test.js
T
byrongamatosandClaude Sonnet 4.6 a1f7ad5e68 test(h3d-carve-1): add class-killer coverage for camBaseDistU, camLowFretPullbackU, _makeGaussTex
Toby r1 finding: three exports in src/geometry.js had no class-killer tests.
Named mutations that were undetected before this commit:
  - camBaseDistU: Math.max(span,4)→span leaves camBaseDistU(0)=65 vs 77
  - camLowFretPullbackU: drop Max(0,..) leaves (10)=-20 vs 0
  - _makeGaussTex: default sigma=0 leaves centre-pixel alpha=0 vs 255

Each test is written to call without the value that makes the mutation
transparent (sigma passed without default; span=0 for the floor case;
fret=10 for the clamp case). Mutation verification run before commit:
all three mutations confirmed caught, all three pass on real code.

Suite (cmd): node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
Base (84d3376): 169/169
Tip:          172/172  (+3 discriminating tests)

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

118 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Class-killer for src/geometry.js — h3d-carve-1.
//
// Uses dynamic import() (not the vm source-scan pattern) so Node actually
// evaluates the ES module and its exports are the real runtime values.
// A refactor that renames geoFretX, changes the uniform/logarithmic
// decision, removes slideTrailEnd, or breaks computeBPM's BPM estimate
// would be caught here before any other test.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const GEOMETRY_JS = path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'src', 'geometry.js');
test('geoFretX returns 0 for fret 0 in both modes', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
assert.strictEqual(geoFretX(0, true), 0, 'uniform: fret 0 must be 0');
assert.strictEqual(geoFretX(0, false), 0, 'logarithmic: fret 0 must be 0');
});
test('geoFretX uniform spacing is linear — fret N is N × fret 1', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
const step = geoFretX(1, true);
assert.ok(step > 0, 'uniform step must be positive');
assert.ok(Math.abs(geoFretX(5, true) - 5 * step) < 1e-9, 'fret 5 must be 5 × step');
assert.ok(Math.abs(geoFretX(12, true) - 12 * step) < 1e-9, 'fret 12 must be 12 × step');
});
test('geoFretX logarithmic spacing is non-linear — frets compress toward the bridge', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
const d1 = geoFretX(1, false);
const d2 = geoFretX(2, false) - geoFretX(1, false);
const d3 = geoFretX(3, false) - geoFretX(2, false);
assert.ok(d1 > d2, 'fret 1 gap must be wider than fret 2 gap (compression toward bridge)');
assert.ok(d2 > d3, 'fret 2 gap must be wider than fret 3 gap');
});
test('geoFretX uniform and logarithmic agree at fret 24 (total board width)', async () => {
const { geoFretX } = await import(GEOMETRY_JS);
// By construction: _fretXUniStep = _fretXLog(24) / 24, so geoFretX(24, uniform)
// equals geoFretX(24, logarithmic). This is the board-width invariant.
const uniWidth = geoFretX(24, true);
const logWidth = geoFretX(24, false);
assert.ok(Math.abs(uniWidth - logWidth) < 1e-9, 'board width must be identical in both modes');
});
test('dZ converts positive dt to a negative Z delta', async () => {
const { dZ } = await import(GEOMETRY_JS);
assert.ok(dZ(1) < 0, 'positive time delta must produce negative Z (notes travel toward camera)');
assert.ok(dZ(0) === 0, 'zero dt must produce zero dZ');
assert.ok(Math.abs(dZ(2) / dZ(1) - 2) < 1e-9, 'dZ must be linear in dt');
});
test('slideTrailEnd returns null for notes with no slide fields', async () => {
const { slideTrailEnd } = await import(GEOMETRY_JS);
assert.strictEqual(slideTrailEnd({}), null);
assert.strictEqual(slideTrailEnd({ sl: -1 }), null, 'negative sl must be ignored');
});
test('slideTrailEnd prefers sl over slu and marks pitched/unpitched correctly', async () => {
const { slideTrailEnd } = await import(GEOMETRY_JS);
assert.deepStrictEqual(slideTrailEnd({ sl: 7 }), { endFret: 7, unpitched: false });
assert.deepStrictEqual(slideTrailEnd({ slu: 5 }), { endFret: 5, unpitched: true });
assert.deepStrictEqual(slideTrailEnd({ sl: 7, slu: 5 }), { endFret: 7, unpitched: false });
});
test('computeBPM returns 120 for degenerate inputs', async () => {
const { computeBPM } = await import(GEOMETRY_JS);
assert.strictEqual(computeBPM(null, 0), 120);
assert.strictEqual(computeBPM([], 0), 120);
assert.strictEqual(computeBPM([{ time: 0 }], 0), 120, 'single beat has no interval');
});
test('computeBPM estimates 120 BPM from evenly-spaced beats', async () => {
const { computeBPM } = await import(GEOMETRY_JS);
// 120 BPM = 0.5 s per beat
const beats = [0, 0.5, 1.0, 1.5, 2.0].map(time => ({ time }));
const bpm = computeBPM(beats, 1.0);
assert.ok(Math.abs(bpm - 120) < 0.01, `expected ~120 BPM, got ${bpm}`);
});
// Toby r1 findings: camBaseDistU, camLowFretPullbackU, _makeGaussTex had no
// class-killer tests. Each test below names the concrete mutation it catches.
test('camBaseDistU clamps span to minimum 4 — span=0 gives 77 not 65', async () => {
// Mutation: Math.max(span,4) → span
// camBaseDistU(0) mutant = 65+0*3 = 65 (wrong); original = 65+4*3 = 77
const { camBaseDistU } = await import(GEOMETRY_JS);
assert.strictEqual(camBaseDistU(0), 77, 'span=0: floor=4 so 65+4*3=77, not 65');
assert.strictEqual(camBaseDistU(10), 95, 'span=10: 65+10*3=95');
});
test('camLowFretPullbackU is clamped to zero — high fret gives 0 not negative', async () => {
// Mutation: drop Math.max(0,...) clamp
// camLowFretPullbackU(10) mutant = (5-10)*4 = -20 (wrong); original = 0
const { camLowFretPullbackU } = await import(GEOMETRY_JS);
assert.strictEqual(camLowFretPullbackU(0), 20, 'fret 0: (5-0)*4=20');
assert.strictEqual(camLowFretPullbackU(5), 0, 'fret 5: (5-5)*4=0');
assert.strictEqual(camLowFretPullbackU(10), 0, 'fret 10: clamped to 0, not -20');
});
test('_makeGaussTex peak alpha is 255 at the centre pixel', async () => {
// Mutation: default sigma changed to 0 → (u-0.5)/0 = NaN chain → all Uint8Array writes
// become 0 (TypedArray coerces NaN to 0). Test calls without explicit sigma so the
// default is exercised directly — changing the default is what is being guarded.
// Use odd width=3: i=1 gives u=0.5 exactly (d=(u-0.5)/sigma=0, peak=1, alpha=255).
const { _makeGaussTex } = await import(GEOMETRY_JS);
let capturedData;
const ThreeStub = {
DataTexture: class { constructor(d) { capturedData = d; } },
RGBAFormat: 1,
LinearFilter: 2,
};
_makeGaussTex(ThreeStub, 3); // no sigma arg — exercises the default (0.28)
// Pixel i=1: RGBA layout [4,5,6,7]; alpha is at index 7
assert.strictEqual(capturedData[7], 255, 'centre pixel (i=1 of w=3) alpha must be 255 at default sigma');
});