import { describe, it, expect } from 'vitest' import { detectClass } from '../../../composables/autoGroup/classify' describe('detectClass', () => { it('detects IQC and EQC by exact vocab match', () => { const result = detectClass( [ ['exp281', 'SAM_SAH', 'Tissues', 'IQC', '003'], ['exp281', 'SAM_SAH', 'Tissues', 'IQC', '056'], ['exp280', 'Experiment', 'EQC', '002'], ], {}, ) const iqc = result.find(c => c.kind === 'iqc') const eqc = result.find(c => c.kind === 'eqc') expect(iqc?.members).toEqual([0, 1]) expect(eqc?.members).toEqual([2]) }) it('detects Biological with subKind from matrix vocab', () => { const result = detectClass( [ ['exp281', 'SAM_SAH', 'Tissues', 'Kidney', 'M10', '085'], ['exp281', 'SAM_SAH', 'Tissues', 'Liver', 'M10', '047'], ['exp280', 'Experiment', 'Plasma', 'M10', '031'], ], {}, ) const tissues = result.find(c => c.kind === 'biological' && c.subKind === 'Tissues') const plasma = result.find(c => c.kind === 'biological' && c.subKind === 'Plasma') expect(tissues?.members).toEqual([0, 1]) expect(plasma?.members).toEqual([2]) }) it('classifies SAHStandard, SAMstandard, ExtractionS, STD1 as Standard (not Biological)', () => { const result = detectClass( [ ['exp281', 'SAM_SAH', 'SAHStandard', '5nM', '002'], ['exp281', 'SAM_SAH', 'SAMstandard', '5nM', '001'], ['exp281', 'SAM_SAH', 'Tissues', 'ExtractionS', '00B'], ['exp', 'STD1', 'inject'], ], {}, ) const std = result.find(c => c.kind === 'standard') expect(std?.members.sort()).toEqual([0, 1, 2, 3]) }) it('classifies SpikeIn and Stock', () => { const result = detectClass( [ ['exp280', 'SK', 'ESwithSpikeIn', '02'], ['exp280', 'SK', 'ESwithSpikeIn', '1to100', 'Stock', '05'], ], {}, ) // SpikeIn matches first, so both go to spike-in (first match wins). const spike = result.find(c => c.kind === 'spike-in') expect(spike?.members.sort()).toEqual([0, 1]) }) it('classifies samples with no recognized vocab as Unknown', () => { const result = detectClass([['foo', 'bar', 'baz']], {}) expect(result[0].kind).toBe('unknown') expect(result[0].members).toEqual([0]) }) it('records classTagPositions for the position(s) that matched vocab', () => { const result = detectClass( [['exp281', 'SAM_SAH', 'Tissues', 'Kidney', 'M10', '085']], {}, ) const tissues = result.find(c => c.kind === 'biological') // Tissues at position 2 (class tag) and Kidney at position 3 (matrix tag). expect(tissues?.classTagPositions).toContain(2) expect(tissues?.classTagPositions).toContain(3) }) it('honors sample_type hint short-circuit', () => { const result = detectClass( [['weird', 'name', 'with', 'no', 'vocab']], { sampleTypeHints: ['qc'] }, ) expect(result[0].kind).toBe('iqc') }) it('does not classify bare A1/M10/QC1/STD1 as Biological', () => { // Names that only contain a `[A-Z]+\d+` token without matrix vocab must fall to Unknown, // not Biological — prevents misclassifying calibration/QC labels. const result = detectClass([['exp', 'foo', 'A1']], {}) expect(result[0].kind).toBe('unknown') }) it('Biological requires an accompanying matrix vocab token', () => { const result = detectClass([['exp', 'Plasma', 'M10']], {}) expect(result[0].kind).toBe('biological') expect(result[0].subKind).toBe('Plasma') }) it('first-match-wins between SpikeIn and Stock when both tokens appear', () => { const result = detectClass( [['exp', 'SK', 'ESwithSpikeIn', '1to100', 'Stock', '05']], {}, ) expect(result[0].kind).toBe('spike-in') }) })