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%
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