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