spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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