From 38b445189b4055816a41f617a06aae883a99413c Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 18 Aug 2026 21:46:46 +0300 Subject: [PATCH] =?UTF-8?q?test(spring):=20[#230]=20boundary=20fuzz=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D1=91=D0=B2=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20+=20round-trip=20=D1=81=D0=B5=D1=82=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B1=D0=BB=D1=8E=D0=B4=D0=B0=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Достающие доказательства из RED-списка #230: сетка краёв M→0/M→1 × tiny/huge времена (конечность или MotionParamError, не NaN); round-trip (ω₀,ζ)↔наблюдаемые на сетке домена (точность 1e-10). Sabotage-proof: мутант знака в L=−ln(mp) краснит 4 теста (проверено диверсией). Контрольный пример тела #230 сверен исполняемо: k=335.7212724332, c=22.9611694937 — совпадение до 1e-10. --- test/spring-observable-constructors.test.ts | 55 +++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/spring-observable-constructors.test.ts b/test/spring-observable-constructors.test.ts index 0861a779..1940b906 100644 --- a/test/spring-observable-constructors.test.ts +++ b/test/spring-observable-constructors.test.ts @@ -192,3 +192,58 @@ describe('validateSpringPhysics ≡ физическая часть validateSpri ); }); }); + +/** + * Boundary fuzz #230: края домена (M→0, M→1, крошечные/огромные времена) + * обязаны давать либо конечные параметры, либо MotionParamError — никогда + * NaN/Infinity в результате (класс «молча неверно»). + */ +describe('boundary fuzz: края домена без NaN/Infinity (#230)', () => { + it('springFromPeak: сетка краёв overshoot × timeToPeak', () => { + for (const mp of [1e-12, 1e-6, 0.001, 0.5, 0.999, 1 - 1e-12]) { + for (const tp of [1e-9, 1e-3, 1, 1e3, 1e9]) { + let p: SpringParams; + try { + p = springFromPeak({ timeToPeak: tp, overshoot: mp }); + } catch (e) { + expect(e).toBeInstanceOf(MotionParamError); + continue; + } + expect(Number.isFinite(p.stiffness)).toBe(true); + expect(Number.isFinite(p.damping)).toBe(true); + expect(p.stiffness).toBeGreaterThan(0); + expect(p.damping).toBeGreaterThanOrEqual(0); + } + } + }); + + it('springFromOscillation: сетка краёв period × halfLife', () => { + for (const period of [1e-9, 1e-3, 1, 1e3, 1e9]) { + for (const halfLife of [1e-9, 1e-3, 1, 1e3, 1e9]) { + let p: SpringParams; + try { + p = springFromOscillation({ period, halfLife }); + } catch (e) { + expect(e).toBeInstanceOf(MotionParamError); + continue; + } + expect(Number.isFinite(p.stiffness)).toBe(true); + expect(Number.isFinite(p.damping)).toBe(true); + } + } + }); + + it('round-trip через (ω₀, ζ) и сырые (m,k,c) на сетке домена', () => { + for (const mp of [0.01, 0.08, 0.3, 0.7]) { + for (const tp of [0.05, 0.22, 2]) { + const p = springFromPeak({ timeToPeak: tp, overshoot: mp }); + const omega0 = Math.sqrt(p.stiffness / p.mass); + const zeta = p.damping / (2 * p.mass * omega0); + const omegaD = omega0 * Math.sqrt(1 - zeta * zeta); + // Прямые наблюдаемые из (ω₀, ζ): t_peak = π/ωd; overshoot = exp(−ζπ/√(1−ζ²)) + expect(Math.PI / omegaD).toBeCloseTo(tp, 10); + expect(Math.exp((-zeta * Math.PI) / Math.sqrt(1 - zeta * zeta))).toBeCloseTo(mp, 10); + } + } + }); +});