SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
4.6 KB · 142 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/test/property.test.ts6 * Purpose: Property-based tests (fast-check) — monotonicity, continuity, anchor identity, determinism7 */89import { describe, expect, it } from "vitest";10import fc from "fast-check";11import { type CounterModel, counterValue } from "../src/index";1213const T0 = "2026-01-01T00:00:00.000Z";14const t0 = Date.parse(T0);1516function model(rateFn: CounterModel["rateFn"], anchorValue = 0): CounterModel {17  return {18    metricId: "prop_metric",19    anchorValue,20    anchorTime: T0,21    rateFn,22    observedAt: T0,23    sourceId: "test_source",24    modelVersion: "test-v1",25    displayHints: { decimals: 0, unit: "units" },26  };27}2829describe("property: cumulative monotonicity", () => {30  it("non-negative linear/piecewise rates never go backwards", () => {31    fc.assert(32      fc.property(33        fc.array(fc.double({ min: 0, max: 1e6, noNaN: true }), { minLength: 1, maxLength: 5 }),34        fc.integer({ min: 0, max: 86_400_000 }),35        fc.integer({ min: 0, max: 86_400_000 }),36        (rates, dtA, dtB) => {37          const segments = rates.map((perSecond, i) => ({38            from: new Date(t0 + i * 3_600_000).toISOString(),39            perSecond,40          }));41          const m = model({ kind: "piecewise", segments });42          const [lo, hi] = dtA <= dtB ? [dtA, dtB] : [dtB, dtA];43          expect(counterValue(m, t0 + hi)).toBeGreaterThanOrEqual(44            counterValue(m, t0 + lo) - 1e-9,45          );46        },47      ),48    );49  });5051  it("monotone spline knots produce monotone values (no overshoot, ever)", () => {52    fc.assert(53      fc.property(54        fc.array(fc.double({ min: 0.001, max: 1e6, noNaN: true }), {55          minLength: 2,56          maxLength: 8,57        }),58        fc.integer({ min: 0, max: 999 }),59        (increments, sample) => {60          let v = 0;61          const knots: Array<[string, number]> = increments.map((inc, i) => {62            v += inc;63            return [new Date(t0 + i * 86_400_000).toISOString(), v];64          });65          const m = model({ kind: "spline", knots }, knots[0]![1]);66          const span = (knots.length - 1) * 86_400_000;67          const tA = t0 + (span * sample) / 1000;68          const tB = t0 + (span * Math.min(sample + 1, 1000)) / 1000;69          expect(counterValue(m, tB)).toBeGreaterThanOrEqual(counterValue(m, tA) - 1e-6);70        },71      ),72    );73  });74});7576describe("property: continuity at piecewise junctions", () => {77  it("value is continuous through every segment boundary", () => {78    fc.assert(79      fc.property(80        fc.array(fc.double({ min: -1e3, max: 1e3, noNaN: true }), {81          minLength: 2,82          maxLength: 6,83        }),84        (rates) => {85          const segments = rates.map((perSecond, i) => ({86            from: new Date(t0 + i * 60_000).toISOString(),87            perSecond,88          }));89          const m = model({ kind: "piecewise", segments });90          for (let i = 1; i < rates.length; i++) {91            const tj = t0 + i * 60_000;92            const before = counterValue(m, tj - 1);93            const after = counterValue(m, tj + 1);94            // Max drift across 2 ms is bounded by max |rate| * 2 ms.95            expect(Math.abs(after - before)).toBeLessThanOrEqual(1e3 * 0.002 + 1e-9);96          }97        },98      ),99    );100  });101});102103describe("property: anchor identity & determinism", () => {104  it("value(anchorTime) === anchorValue for every kind", () => {105    fc.assert(106      fc.property(107        fc.double({ min: -1e9, max: 1e9, noNaN: true }),108        fc.double({ min: -1e3, max: 1e3, noNaN: true }),109        (anchorValue, perSecond) => {110          for (const rateFn of [111            { kind: "linear", perSecond } as const,112            {113              kind: "seasonal",114              base: perSecond,115              harmonics: [{ period: "year", order: 1, amplitude: 1, phase: 0.5 }],116            } as const,117          ]) {118            const m = model(rateFn, anchorValue);119            expect(counterValue(m, t0)).toBeCloseTo(anchorValue, 6);120          }121        },122      ),123    );124  });125126  it("same model + same t ⇒ identical value (repeatable)", () => {127    fc.assert(128      fc.property(fc.integer({ min: -1e9, max: 1e9 }), (dt) => {129        const m = model({130          kind: "seasonal",131          base: 3,132          harmonics: [133            { period: "year", order: 1, amplitude: 2, phase: 1 },134            { period: "day", order: 1, amplitude: 0.5, phase: 2 },135          ],136        });137        expect(counterValue(m, t0 + dt)).toBe(counterValue(m, t0 + dt));138      }),139    );140  });141});142