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.5 KB · 111 lines typescript
Raw Blame History
1import { describe, expect, it } from 'vitest';2import { BIOMARKER_KINDS, BIOMARKER_KIND_LABEL, BIOMARKER_SEED, BIOMARKER_SOURCES, NCIT_VERIFICATION, buildMeasurement, seedGeneSymbols } from '../src/seed-data/biomarkers.js';34/**5 * Pure integrity checks on the curated biomarker catalogue (no database). The NCIt codes themselves6 * were verified live against the EVS REST API (see the file header); these tests guard the shape7 * and the editorial rules (CLAUDE.md §8: curated metadata only, verified codes, no advice).8 */9describe('BIOMARKER_SEED', () => {10  it('covers the canonical catalogue (≥ 45 entries) with unique slugs and NCIt codes', () => {11    expect(BIOMARKER_SEED.length).toBeGreaterThanOrEqual(45);12    const slugs = BIOMARKER_SEED.map((b) => b.slug);13    expect(new Set(slugs).size).toBe(slugs.length);14    const codes = BIOMARKER_SEED.map((b) => b.ncitCode);15    expect(new Set(codes).size).toBe(codes.length);16  });1718  it('uses only the frozen kind vocabulary and every kind has a label', () => {19    for (const b of BIOMARKER_SEED) expect(BIOMARKER_KINDS, b.slug).toContain(b.kind);20    for (const k of BIOMARKER_KINDS) expect(BIOMARKER_KIND_LABEL[k]).toBeTruthy();21  });2223  it('carries a well-formed, biomarker-level NCIt concept on every row', () => {24    for (const b of BIOMARKER_SEED) {25      expect(b.ncitCode, b.slug).toMatch(/^C\d{3,7}$/);26      expect(b.ncitName.length, b.slug).toBeGreaterThan(3);27      expect(b.ncitConceptKind).toBe('biomarker');28      // Never the bare gene concept ("EGFR Gene") — a biomarker/alteration concept is required29      // ("BCR/ABL1 Fusion Gene" is a fusion concept and passes).30      expect(b.ncitName, b.slug).not.toMatch(/^[A-Z0-9]+ Gene$/);31    }32    expect(NCIT_VERIFICATION.ncitVersion).toMatch(/^\d{2}\.\d{2}[a-z]$/);33    expect(NCIT_VERIFICATION.verifiedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/);34  });3536  it('is slug-safe and names anchor genes or an explicit gene list (or documents why not)', () => {37    for (const b of BIOMARKER_SEED) {38      expect(b.slug, b.slug).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);39      if (b.geneSymbol) expect(b.geneSymbol).toMatch(/^[A-Z0-9-]+$/);40      for (const g of b.genes ?? []) expect(g, b.slug).toMatch(/^[A-Z0-9-]+$/);41      if (!b.geneSymbol && !(b.genes?.length)) {42        // Non-gene markers must say so in their notes (rendered as the empty-state reason).43        expect(b.notes, `${b.slug} has no gene and no note`).toBeTruthy();44        expect(['tmb', 'hrd', 'ctdna', 'msi', 'signature', 'other', 'methylation', 'gene_mutation']).toContain(b.kind);45      }46    }47  });4849  it('describes neutrally: one sentence, no dosing, no individual advice', () => {50    for (const b of BIOMARKER_SEED) {51      expect(b.description.trim().endsWith('.'), b.slug).toBe(true);52      expect(b.description, b.slug).not.toMatch(/\b\d+\s?mg\b|\bdose\b|\bdosing\b|\byou should\b|\bpatients should\b|\brecommended\b/i);53      expect(b.description, b.slug).toMatch(/diagnostic|predictive|prognostic|target|eligibility|screening|subtyping|specimen|marker/i);54    }55  });5657  it('cites only verified authority pages and keeps aliases unique', () => {58    for (const b of BIOMARKER_SEED) {59      expect(b.sources.length, b.slug).toBeGreaterThan(0);60      for (const k of b.sources) expect(BIOMARKER_SOURCES[k].url).toMatch(/^https:\/\/(www\.)?(fda\.gov|cancer\.gov)\//);61      expect(new Set(b.aliases).size, b.slug).toBe(b.aliases.length);62      expect(b.assays.length, b.slug).toBeGreaterThan(0);63    }64  });6566  it('flags tumor-agnostic only for the markers with FDA tissue-agnostic indications', () => {67    const flagged = BIOMARKER_SEED.filter((b) => b.tumorAgnostic).map((b) => b.slug).sort();68    expect(flagged).toEqual(['braf-v600e', 'dmmr', 'her2', 'msi-h', 'ntrk-fusion', 'ret-fusion', 'tmb-h']);69    // Each flagged marker needs indication terms so the UI can show the actual approval rows.70    for (const b of BIOMARKER_SEED.filter((x) => x.tumorAgnostic)) expect(b.indicationTerms?.length, b.slug).toBeGreaterThan(0);71  });7273  it('states the MSI-H / dMMR relationship on both entries', () => {74    const msi = BIOMARKER_SEED.find((b) => b.slug === 'msi-h')!;75    const dmmr = BIOMARKER_SEED.find((b) => b.slug === 'dmmr')!;76    expect(msi.notes).toMatch(/dMMR/);77    expect(dmmr.notes).toMatch(/MSI-H/);78    expect(msi.genes).toEqual(['MLH1', 'MSH2', 'MSH6', 'PMS2']);79    expect(dmmr.genes).toEqual(msi.genes);80  });81});8283describe('buildMeasurement', () => {84  it('produces the jsonb shape the queries read (aliases, genes, variantSlugs, terms, verification)', () => {85    const her2 = BIOMARKER_SEED.find((b) => b.slug === 'her2')!;86    const m = buildMeasurement(her2);87    expect(m.aliases).toContain('ERBB2');88    expect(m.tumorAgnostic).toBe(true);89    expect(m.ncit).toEqual({ code: 'C68748', name: 'HER2/Neu Positive', conceptKind: 'biomarker' });90    expect(m.verification).toBe(NCIT_VERIFICATION);91    expect(m.sources[0]).toHaveProperty('url');92    expect(m.genes).toEqual([]);93    expect(m.variantSlugs).toContain('erbb2-amplification');94    expect(m.scoring).toMatch(/3\+/);95  });96  it('omits absent optional fields instead of writing null', () => {97    const cd19 = BIOMARKER_SEED.find((b) => b.slug === 'cd19')!;98    const m = buildMeasurement(cd19);99    expect('scoring' in m).toBe(false);100    expect(m.tumorAgnostic).toBe(false);101  });102});103104describe('seedGeneSymbols', () => {105  it('returns every anchor and multi-gene symbol once, sorted', () => {106    const s = seedGeneSymbols();107    expect(s).toEqual([...new Set(s)].sort());108    for (const g of ['ERBB2', 'EGFR', 'MLH1', 'PMS2', 'NTRK3', 'BCR', 'ABL1', 'TP53']) expect(s).toContain(g);109  });110});111