From ed96a8a5d1c4d4f7cce03b89fa0e761d892bba54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 22 Jun 2026 09:13:02 +0200 Subject: [PATCH] test(rollups): assert regression parity relatively, not at absolute 9 dp The DST-boundary parity case seeds two readings on the same UTC day, so the windowed compose folds a closed form over a large epoch-day x (~20540). The n*Sxx - Sx^2 denominator subtracts two ~1e10 magnitudes and loses about ten significant digits to cancellation before the divide, so an absolute toBeCloseTo at 9 dp is the wrong gauge for a slope that small. The populator sums per-raw-reading accumulators grouped by day, so both paths regress over the identical raw-row x set and agree mathematically; the residual is float summation order alone. Switch all parity assertions to a shared relative tolerance (1e-9 of scale), far tighter than the 2-3 dp the read tier rounds to. --- ...llup-regression-parity.integration.test.ts | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/tests/integration/rollup-regression-parity.integration.test.ts b/tests/integration/rollup-regression-parity.integration.test.ts index aabc32f02..81126991e 100644 --- a/tests/integration/rollup-regression-parity.integration.test.ts +++ b/tests/integration/rollup-regression-parity.integration.test.ts @@ -14,7 +14,20 @@ * diverge), folds the rollups, then asserts: * * 1. `composeRegression` over the folded DAY-bucket accumulators equals - * `REGR_SLOPE` / `REGR_R2` / `STDDEV_POP` over the raw rows to ≤ 1e-9. + * `REGR_SLOPE` / `REGR_R2` / `STDDEV_POP` over the raw rows to a tight + * relative tolerance. + * + * Tolerance note: both paths fold the SAME closed form over the SAME raw-row + * epoch-day x set, so they agree mathematically. They disagree only in float + * summation order — Postgres's parallel aggregate accumulators vs. the + * textbook `n·Σxy − Σx·Σy` closed form `composeRegression` evaluates from the + * stored sums. With a UTC epoch-day x-axis (x ≈ 20 540), `n·Σxx − Σx²` is a + * difference of two ~1e10 magnitudes, so the closed form sheds ~10 significant + * digits to cancellation before the divide. That makes an ABSOLUTE 9-dp + * `toBeCloseTo` the wrong gauge for a slope this small — the residual is float + * reorder noise, not a data divergence. Parity is asserted RELATIVELY at + * 1e-9, which is far tighter than the 2–3 dp the read tier ultimately rounds + * the windowed slope / r² / sd to. * 2. The source-collapse runs BEFORE accumulator summation — a dual-source * day contributes only the canonical source's accumulators, matching a * live regression over the canonical-source rows. @@ -38,6 +51,17 @@ vi.mock("@/lib/jobs/boss-instance", () => ({ getGlobalBoss: vi.fn(() => null), })); +/** + * Relative parity assertion for two values that are mathematically equal but + * differ only by float summation order (see the tolerance note in the file + * header). Absolute `toBeCloseTo` is the wrong gauge across magnitudes; this + * pins |a − b| ≤ rel · max(|a|, |b|, 1) with a tight 1e-9 relative bound. + */ +function expectParity(composed: number, live: number, rel = 1e-9): void { + const scale = Math.max(Math.abs(composed), Math.abs(live), 1); + expect(Math.abs(composed - live)).toBeLessThanOrEqual(rel * scale); +} + /** Live REGR row shape (one per type over the seeded window). */ interface LiveRegrRow { slope: number | null; @@ -186,10 +210,10 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => { expect(live.slope).not.toBeNull(); expect(composed.slope).not.toBeNull(); - // Bit-identical parity — the closed form is the same on both paths. - expect(composed.slope!).toBeCloseTo(live.slope!, 9); - expect(composed.r2!).toBeCloseTo(live.r2!, 9); - expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9); + // Same closed form on both paths — parity holds to float reorder noise. + expectParity(composed.slope!, live.slope!); + expectParity(composed.r2!, live.r2!); + expectParity(composed.sdPop!, live.sd_pop!); }); it("source-collapse precedes summation: composed RHR equals live REGR over the WHOOP source only", async () => { @@ -250,9 +274,9 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => { const composed = composeRegression(acc); expect(Number(live.n)).toBe(days.length); - expect(composed.slope!).toBeCloseTo(live.slope!, 9); - expect(composed.r2!).toBeCloseTo(live.r2!, 9); - expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9); + expectParity(composed.slope!, live.slope!); + expectParity(composed.r2!, live.r2!); + expectParity(composed.sdPop!, live.sd_pop!); // The blend would be wrong: a regression over BOTH sources differs from // the WHOOP-only one, proving the collapse-before-sum matters. @@ -296,8 +320,11 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => { const composed = composeRegression(acc); expect(Number(live.n)).toBe(seed.length); - expect(composed.slope!).toBeCloseTo(live.slope!, 9); - expect(composed.r2!).toBeCloseTo(live.r2!, 9); - expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9); + // The two same-UTC-day DST readings (00:30Z + 02:30Z) make this the + // multi-reading-per-day case where the large epoch-day x drives the worst + // closed-form cancellation — relative parity is the honest gauge. + expectParity(composed.slope!, live.slope!); + expectParity(composed.r2!, live.r2!); + expectParity(composed.sdPop!, live.sd_pop!); }); });