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%
5.1 KB · 122 lines typescript
Raw Blame History
1import { describe, expect, it } from 'vitest';2import { STOP_REASON_CATEGORIES, STOP_REASON_RULES, classifyStopReason, isStopReasonCategory, stopReasonBreakdown } from './trial-stop-reasons.js';34describe('classifyStopReason', () => {5  it('returns not_stated for null, undefined and blank text', () => {6    for (const v of [null, undefined, '', '   ', '\n']) {7      const r = classifyStopReason(v);8      expect(r.category).toBe('not_stated');9      expect(r.matched).toEqual([]);10    }11  });1213  it('returns other_stated when text is present but no rule matches (never infers)', () => {14    const r = classifyStopReason('Sponsor is focusing on studies which can enable registration of duvelisib');15    expect(r.category).toBe('other_stated');16    expect(r.matched).toEqual([]);17  });1819  it.each([20    ['slow accrual', 'enrollment'],21    ['Poor recruitment', 'enrollment'],22    ['low enrollment', 'enrollment'],23    ['Low enrolment (UK spelling)', 'enrollment'],24    ['No funding', 'funding'],25    ['Costs for antibody production rose; supplemental funding request not approved.', 'funding'],26    ['Financial constraints', 'funding'],27    ['Budget cuts', 'funding'],28    ['Business decision', 'sponsor_decision'],29    ["Sponsor's decision", 'sponsor_decision'],30    ['Decision of the sponsor', 'sponsor_decision'],31    ['Change in development strategy', 'sponsor_decision'],32    ['Portfolio prioritization', 'sponsor_decision'],33    ['Company reorganisation', 'sponsor_decision'],34    ['Terminated due to unacceptable toxicity', 'safety'],35    ['Safety concerns', 'safety'],36    ['Serious adverse events', 'safety'],37    ['Lack of efficacy', 'efficacy'],38    ['Stopped for futility', 'efficacy'],39    ['Lack of clinical benefit', 'efficacy'],40    ['Stopped after interim analysis', 'efficacy'],41    ['Drug supply issues', 'drug_supply'],42    ['Drug availability', 'drug_supply'],43    ['Drug manufacturing process and procedure review', 'drug_supply'],44    ['PI left the institution', 'investigator'],45    ['Investigator relocated', 'investigator'],46    ['COVID-19 pandemic', 'covid'],47    ['Site closed during the pandemic', 'covid'],48  ])('classifies %j as %s', (text, expected) => {49    expect(classifyStopReason(text).category).toBe(expected);50  });5152  it('is case-insensitive', () => {53    expect(classifyStopReason('SLOW ACCRUAL').category).toBe('enrollment');54    expect(classifyStopReason('covid').category).toBe('covid');55  });5657  it('uses word boundaries so unrelated words do not match', () => {58    // "refund" must not match funding; "companion" must not match company; "recruit" inside "unrecruitable" must not match.59    expect(classifyStopReason('refund issued to participants').category).toBe('other_stated');60    expect(classifyStopReason('companion diagnostic unavailable').category).toBe('other_stated');61  });6263  it('applies precedence and reports every matched category', () => {64    const r = classifyStopReason('slow enrollment and lack of funding');65    expect(r.category).toBe('enrollment');66    expect(r.matched).toEqual(['enrollment', 'funding']);67    const s = classifyStopReason('Enrollment halted for safety reasons');68    expect(s.category).toBe('safety');69    expect(s.matched).toEqual(['safety', 'enrollment']);70    const t = classifyStopReason('Business decision: slow accrual');71    expect(t.category).toBe('enrollment');72    expect(t.matched).toEqual(['enrollment', 'sponsor_decision']);73  });7475  it('is deterministic and carries the rules version', () => {76    const a = classifyStopReason('slow accrual');77    const b = classifyStopReason('slow accrual');78    expect(a).toEqual(b);79    expect(a.rulesVersion).toBe('ci-stop-reasons-v1');80  });81});8283describe('rules table', () => {84  it('lists every ruled category exactly once, in precedence order, with the broadest last', () => {85    const cats = STOP_REASON_RULES.map((r) => r.category);86    expect(new Set(cats).size).toBe(cats.length);87    expect(cats[cats.length - 1]).toBe('sponsor_decision');88    expect(STOP_REASON_CATEGORIES.slice(0, cats.length)).toEqual(cats);89  });90  it('documents at least one keyword per pattern group', () => {91    for (const r of STOP_REASON_RULES) {92      expect(r.keywords.length).toBeGreaterThan(0);93      expect(r.patterns.length).toBeGreaterThan(0);94    }95  });96});9798describe('stopReasonBreakdown', () => {99  it('counts every category with zeros and sums to the input size', () => {100    const b = stopReasonBreakdown(['slow accrual', null, '', 'No funding', 'unknown reason', 'toxicity']);101    expect(Object.keys(b)).toEqual([...STOP_REASON_CATEGORIES]);102    expect(b.enrollment).toBe(1);103    expect(b.not_stated).toBe(2);104    expect(b.funding).toBe(1);105    expect(b.other_stated).toBe(1);106    expect(b.safety).toBe(1);107    expect(Object.values(b).reduce((s, n) => s + n, 0)).toBe(6);108  });109  it('is empty-safe', () => {110    const b = stopReasonBreakdown([]);111    expect(Object.values(b).every((n) => n === 0)).toBe(true);112  });113});114115describe('isStopReasonCategory', () => {116  it('accepts known categories and rejects others', () => {117    expect(isStopReasonCategory('enrollment')).toBe(true);118    expect(isStopReasonCategory('not_stated')).toBe(true);119    expect(isStopReasonCategory('bogus')).toBe(false);120  });121});122