import { describe, expect, it } from 'vitest'; import { DRUG_SALT_TOKENS, MIN_SHARED_ALIASES, detectDuplicateCandidates, moleculeKey, pickKeep, type DrugLite } from './drug-duplicates.js'; const d = (id: string, name: string, aliases: Array<[string, string]> = []): DrugLite => ({ id, name, aliases: aliases.map(([normalized, aliasType]) => ({ normalized, aliasType })) }); describe('drug duplicates — molecule key', () => { it('strips trailing salt tokens and parentheticals, keeps the first token', () => { expect(moleculeKey('Imatinib Mesylate')).toBe('imatinib'); expect(moleculeKey('Erlotinib Hydrochloride')).toBe('erlotinib'); expect(moleculeKey('Sorafenib Tosylate')).toBe('sorafenib'); expect(moleculeKey('Abiraterone Acetate')).toBe('abiraterone'); expect(moleculeKey('Trastuzumab Deruxtecan')).toBe('trastuzumab deruxtecan'); expect(moleculeKey('Imatinib (Imatinib Mesylate)')).toBe('imatinib'); expect(moleculeKey('Sodium')).toBe('sodium'); // never empties a name expect(moleculeKey('JQ1')).toBe('jq1'); }); it('the salt list is the connectors list (kept in sync by hand)', () => { expect(DRUG_SALT_TOKENS).toContain('mesylate'); expect(DRUG_SALT_TOKENS).toContain('tosylate'); expect(new Set(DRUG_SALT_TOKENS).size).toBe(DRUG_SALT_TOKENS.length); }); }); describe('drug duplicates — detector', () => { it('salt-form pairs: keep the base molecule, merge the salt', () => { const c = detectDuplicateCandidates([d('CI-DRUG-00000002', 'Imatinib Mesylate'), d('CI-DRUG-00000001', 'Imatinib'), d('CI-DRUG-00000003', 'Osimertinib')]); expect(c).toHaveLength(1); expect(c[0]).toMatchObject({ keepId: 'CI-DRUG-00000001', mergeId: 'CI-DRUG-00000002', reason: 'salt_form' }); expect(c[0]!.evidence).toMatchObject({ moleculeKey: 'imatinib', keepName: 'Imatinib', mergeName: 'Imatinib Mesylate' }); }); it('identical names → same_name, smaller id kept', () => { const c = detectDuplicateCandidates([d('CI-DRUG-00000009', 'Regorafenib'), d('CI-DRUG-00000004', 'Regorafenib')]); expect(c).toEqual([expect.objectContaining({ keepId: 'CI-DRUG-00000004', mergeId: 'CI-DRUG-00000009', reason: 'same_name' })]); }); it('shared generic/brand/development_code aliases (≥ 2) → candidate; synonyms and single shared alias do not count', () => { const a = d('CI-DRUG-00000010', 'Drug A', [['tagrisso', 'brand'], ['azd9291', 'development_code'], ['foo', 'synonym']]); const b = d('CI-DRUG-00000011', 'Drug Beta', [['tagrisso', 'brand'], ['azd9291', 'development_code'], ['foo', 'synonym']]); const c1 = d('CI-DRUG-00000012', 'Drug C', [['tagrisso', 'brand'], ['bar', 'synonym'], ['foo', 'synonym']]); const out = detectDuplicateCandidates([a, b, c1]); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ keepId: 'CI-DRUG-00000010', mergeId: 'CI-DRUG-00000011', reason: 'shared_aliases' }); expect((out[0]!.evidence.sharedAliases as string[]).length).toBeGreaterThanOrEqual(MIN_SHARED_ALIASES); }); it('one candidate per pair, strongest reason wins', () => { const a = d('CI-DRUG-00000020', 'Sorafenib', [['nexavar', 'brand'], ['bay 43 9006', 'development_code']]); const b = d('CI-DRUG-00000021', 'Sorafenib Tosylate', [['nexavar', 'brand'], ['bay 43 9006', 'development_code']]); const out = detectDuplicateCandidates([a, b]); expect(out).toHaveLength(1); expect(out[0]!.reason).toBe('salt_form'); }); it('pickKeep prefers an INN over a development code, then fewer tokens, then the shorter name, then the smaller id', () => { expect(pickKeep(d('CI-DRUG-00000566', 'TAK-788'), d('CI-DRUG-00000382', 'Mobocertinib')).keep.id).toBe('CI-DRUG-00000382'); expect(pickKeep(d('CI-DRUG-00000499', 'RDEA 119'), d('CI-DRUG-00000511', 'Refametinib')).keep.id).toBe('CI-DRUG-00000511'); expect(pickKeep(d('CI-DRUG-00000002', 'Erlotinib Hydrochloride'), d('CI-DRUG-00000001', 'Erlotinib')).keep.id).toBe('CI-DRUG-00000001'); expect(pickKeep(d('CI-DRUG-00000002', 'Abc'), d('CI-DRUG-00000001', 'Abcd')).keep.id).toBe('CI-DRUG-00000002'); expect(pickKeep(d('CI-DRUG-00000002', 'Same'), d('CI-DRUG-00000001', 'Same')).keep.id).toBe('CI-DRUG-00000001'); }); it('no duplicates → empty', () => { expect(detectDuplicateCandidates([d('CI-DRUG-00000001', 'Osimertinib'), d('CI-DRUG-00000002', 'Pembrolizumab')])).toEqual([]); expect(detectDuplicateCandidates([])).toEqual([]); }); });