SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
4.3 KB · 98 lines typescript
Raw Blame History
1import { describe, expect, it } from 'vitest';2import { TRIAL_INTEL_ACTIVE_STATUSES, TRIAL_INTEL_THRESHOLDS, TRIAL_INTELLIGENCE_FORMULA_VERSION, burdenNormalized, growthWindows, growthYoy, hhi, terminationShare } from './trial-intelligence.js';34describe('growthWindows', () => {5  it('builds two adjacent, non-overlapping 12-month windows ending on asOf (exclusive)', () => {6    const w = growthWindows('2026-09-11');7    expect(w).toEqual({ asOf: '2026-09-11', new12m: { from: '2025-09-11', to: '2026-09-11' }, prior12m: { from: '2024-09-11', to: '2025-09-11' } });8    expect(w.prior12m.to).toBe(w.new12m.from);9  });10  it('clamps month-end days (leap day, 31st)', () => {11    expect(growthWindows('2024-02-29').new12m.from).toBe('2023-02-28');12    expect(growthWindows('2025-03-31').prior12m.from).toBe('2023-03-31');13    expect(growthWindows('2025-05-31').new12m.from).toBe('2024-05-31');14    expect(growthWindows('2025-12-31').new12m.from).toBe('2024-12-31');15  });16  it('rejects malformed dates', () => {17    expect(() => growthWindows('2026-9-1')).toThrow();18    expect(() => growthWindows('nope')).toThrow();19  });20});2122describe('growthYoy', () => {23  it('computes (new − prior) / prior', () => {24    expect(growthYoy(120, 100)).toBeCloseTo(0.2);25    expect(growthYoy(80, 100)).toBeCloseTo(-0.2);26    expect(growthYoy(100, 100)).toBe(0);27  });28  it('is null under the prior-window threshold (default 20)', () => {29    expect(growthYoy(50, 19)).toBeNull();30    expect(growthYoy(50, 20)).toBeCloseTo(1.5);31    expect(growthYoy(5, 0)).toBeNull();32    expect(growthYoy(5, 2, 2)).toBeCloseTo(1.5);33  });34  it('threshold matches the persisted constant', () => {35    expect(TRIAL_INTEL_THRESHOLDS.growthMinPriorTrials).toBe(20);36  });37});3839describe('hhi', () => {40  it('is 1 for a single holder and 1/n for n equal holders', () => {41    expect(hhi([42])).toBe(1);42    expect(hhi([10, 10, 10, 10])).toBeCloseTo(0.25);43  });44  it('is Σ share² for an uneven distribution', () => {45    // shares 0.5, 0.3, 0.2 → 0.25 + 0.09 + 0.0446    expect(hhi([50, 30, 20])).toBeCloseTo(0.38);47  });48  it('is null when empty or all zero, ignores negative / non-finite counts', () => {49    expect(hhi([])).toBeNull();50    expect(hhi([0, 0])).toBeNull();51    expect(hhi([5, -1, Number.NaN, 5])).toBeCloseTo(0.5);52  });53});5455describe('terminationShare', () => {56  it('computes (terminated + withdrawn) / (completed + terminated + withdrawn)', () => {57    expect(terminationShare(70, 20, 10)).toBeCloseTo(0.3);58    expect(terminationShare(100, 0, 0)).toBe(0);59  });60  it('is null when the terminal denominator is under 30', () => {61    expect(terminationShare(20, 5, 4)).toBeNull();62    expect(terminationShare(20, 5, 5)).toBeCloseTo(1 / 3);63    expect(terminationShare(0, 0, 0)).toBeNull();64  });65  it('threshold matches the persisted constant', () => {66    expect(TRIAL_INTEL_THRESHOLDS.terminationMinTerminalTrials).toBe(30);67  });68});6970describe('burdenNormalized', () => {71  it('computes trials per 1,000 deaths and per 100,000 cases', () => {72    const r = burdenNormalized(500, 125_000, 250_000);73    expect(r.per1000Deaths).toBeCloseTo(4);74    expect(r.per100kCases).toBeCloseTo(200);75  });76  it('is null when deaths are missing or under 100', () => {77    expect(burdenNormalized(10, null, 1000)).toEqual({ per1000Deaths: null, per100kCases: null });78    expect(burdenNormalized(10, 99, 1000)).toEqual({ per1000Deaths: null, per100kCases: null });79    expect(burdenNormalized(10, 100, 1000).per1000Deaths).toBeCloseTo(100);80  });81  it('leaves the incidence ratio null when incidence is missing or zero', () => {82    expect(burdenNormalized(10, 1000, null).per100kCases).toBeNull();83    expect(burdenNormalized(10, 1000, 0).per100kCases).toBeNull();84  });85  it('zero active trials gives zero, never null (a true zero)', () => {86    expect(burdenNormalized(0, 1000, 2000)).toEqual({ per1000Deaths: 0, per100kCases: 0 });87  });88});8990describe('constants', () => {91  it('formula version and active statuses match entity_counters', () => {92    expect(TRIAL_INTELLIGENCE_FORMULA_VERSION).toBe('ci-trial-intel-v1');93    expect([...TRIAL_INTEL_ACTIVE_STATUSES]).toEqual(['RECRUITING', 'NOT_YET_RECRUITING', 'ENROLLING_BY_INVITATION', 'ACTIVE_NOT_RECRUITING']);94    expect(TRIAL_INTEL_THRESHOLDS.burdenGeography).toBe('USA');95    expect(TRIAL_INTEL_THRESHOLDS.terminationSince).toBe('2010-01-01');96  });97});98