import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; const source = fs.readFileSync(new URL('../www/views/phase_wheel.js', import.meta.url), 'utf8'); const context = vm.createContext({ Number, Math }); const smoothingSource = source.match(/export function smoothingAlpha[\s\S]*?\n\}/)?.[0]; assert.ok(smoothingSource, 'time-based smoothing must remain testable'); vm.runInContext(smoothingSource.replace('export function', 'function'), context); const oneFrame = context.smoothingAlpha(1 / 60, 0.2); const twoFrames = 1 - Math.pow(1 - oneFrame, 2); assert.ok(Math.abs(twoFrames - context.smoothingAlpha(1 / 30, 0.2)) < 1e-12, 'smoothing over equal real time must not depend on the display frame rate'); assert.doesNotMatch(source, /auto\.gain \* PHASE_AGC_BASE_GAIN/, 'AGC target gain must not be multiplied a second time'); const maxAngleStep = Number( source.match(/PHASE_TRAIL_MAX_ANGLE_STEP_RAD\s*=\s*\(([\d.]+)\s*\*\s*Math\.PI\)\s*\/\s*180/)?.[1], ) * Math.PI / 180; assert.ok(Number.isFinite(maxAngleStep) && maxAngleStep <= (2 * Math.PI) / 180, 'phase trail interpolation must limit angular steps to at most two degrees'); const interpolationSource = source.match(/function appendPhaseTrailSegment[\s\S]*?\n\}/)?.[0]; assert.ok(interpolationSource, 'phase trail must interpolate polar segments'); const trailContext = vm.createContext({ Number, Math }); const trailHelpers = [ source.match(/const PHASE_TRAIL_MAX_ANGLE_STEP_RAD\s*=.*?;/)?.[0], source.match(/function radToDeg[\s\S]*?\n\}/)?.[0], source.match(/function trailColorForAngle[\s\S]*?\n\}/)?.[0], source.match(/function wrapAngle[\s\S]*?\n\}/)?.[0], source.match(/function lerp[\s\S]*?\n\}/)?.[0], source.match(/function createPhaseTrailPoint[\s\S]*?\n\}/)?.[0], interpolationSource, ]; assert.ok(trailHelpers.every(Boolean), 'phase trail interpolation helpers must remain testable'); vm.runInContext(trailHelpers.join('\n'), trailContext); const wheel = { cx: 0, cy: 0, radius: 100 }; const trail = [trailContext.createPhaseTrailPoint(wheel, -Math.PI / 2, 1, 0)]; trailContext.appendPhaseTrailSegment(trail, wheel, trail[0], Math.PI / 2, 1, 100); assert.equal(trail.length, 91, 'a 180-degree phase change must be split into 90 two-degree segments'); for (let index = 1; index < trail.length; index++) { const previousAngle = Math.atan2(trail[index - 1].y, trail[index - 1].x); const currentAngle = Math.atan2(trail[index].y, trail[index].x); const angularStep = Math.abs(Math.atan2( Math.sin(currentAngle - previousAngle), Math.cos(currentAngle - previousAngle), )); assert.ok(angularStep <= maxAngleStep + 1e-12, 'interpolated trail segments must respect the maximum angular step'); assert.ok(Math.abs(Math.hypot(trail[index].x, trail[index].y) - wheel.radius) < 1e-9, 'constant-radius phase movement must remain on a circular arc'); } assert.match(source, /PHASE_TRAIL_MAX_POINTS/, 'interpolated phase trail history must remain bounded'); console.log('phase wheel regression tests passed');