import { describe, it, expect } from 'vitest' import { composeGroups } from '../../../composables/autoGroup/compose' import type { ClassSchema } from '../../../types/auto-group' const tissueSchema: ClassSchema = { classKind: 'biological', subKind: 'Tissues', columns: [ { index: 0, name: 'Tissue', sourceIndices: [0], uniqueValues: ['Kidney', 'Liver'], cardinality: 2, role: 'factor' }, { index: 1, name: 'Animal', sourceIndices: [1], uniqueValues: ['M1', 'M2', 'M3'], cardinality: 3, role: 'replicate' }, ], groupBy: [0], } const iqcSchema: ClassSchema = { classKind: 'iqc', columns: [ { index: 0, name: 'IQC', sourceIndices: [0], uniqueValues: ['IQC'], cardinality: 1, role: 'class-tag' }, { index: 1, name: 'Run', sourceIndices: [1], uniqueValues: ['001', '002'], cardinality: 2, role: 'run-order' }, ], groupBy: [], } describe('composeGroups', () => { it('builds groups by enabled factor columns, ignoring replicate and run-order roles', () => { const result = composeGroups({ tokenizedSamples: [ ['Kidney', 'M1'], ['Kidney', 'M2'], ['Liver', 'M1'], ], sampleNames: ['s1', 's2', 's3'], schemas: { 'biological:Tissues': tissueSchema }, classes: [ { kind: 'biological', subKind: 'Tissues', label: 'Biological / Tissues', members: [0, 1, 2], classTagPositions: [], disposition: 'group', }, ], }) // A single experimental class needs no disambiguating prefix: group names // start directly at the first enabled factor layer. expect(result.experimentalGroups!.map(g => g.name).sort()).toEqual([ 'Kidney', 'Liver', ]) expect( result.experimentalGroups!.find(g => g.name === 'Kidney')!.samples, ).toEqual(['s1', 's2']) }) it('routes overlay classes to qcGroups (separate from experimental)', () => { const result = composeGroups({ tokenizedSamples: [ ['Kidney', 'M1'], ['IQC', '001'], ['IQC', '002'], ], sampleNames: ['bio1', 'iqc1', 'iqc2'], schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema }, classes: [ { kind: 'biological', subKind: 'Tissues', label: 'Biological / Tissues', members: [0], classTagPositions: [], disposition: 'group', }, { kind: 'iqc', label: 'IQC', members: [1, 2], classTagPositions: [0], disposition: 'overlay', }, ], }) expect(result.qcGroups!.length).toBe(1) expect(result.qcGroups![0].name).toBe('IQC') expect(result.qcGroups![0].samples).toEqual(['iqc1', 'iqc2']) expect(result.experimentalGroups!.length).toBe(1) }) it('routes excluded-disposition classes to excludedSamples', () => { const result = composeGroups({ tokenizedSamples: [ ['Kidney', 'M1'], ['IQC', '001'], ], sampleNames: ['bio1', 'iqc1'], schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema }, classes: [ { kind: 'biological', subKind: 'Tissues', label: 'Biological / Tissues', members: [0], classTagPositions: [], disposition: 'group', }, { kind: 'iqc', label: 'IQC', members: [1], classTagPositions: [0], disposition: 'exclude', }, ], }) expect(result.excludedSamples).toEqual(['iqc1']) expect(result.qcGroups!.length).toBe(0) }) it('legacy groups field equals experimentalGroups concat qcGroups', () => { const result = composeGroups({ tokenizedSamples: [ ['Kidney', 'M1'], ['IQC', '001'], ], sampleNames: ['bio1', 'iqc1'], schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema }, classes: [ { kind: 'biological', subKind: 'Tissues', label: 'Biological / Tissues', members: [0], classTagPositions: [], disposition: 'group', }, { kind: 'iqc', label: 'IQC', members: [1], classTagPositions: [0], disposition: 'overlay', }, ], }) const concatNames = [ ...result.experimentalGroups!.map(g => g.name), ...result.qcGroups!.map(g => g.name), ] expect(result.groups.map(g => g.name)).toEqual(concatNames) }) it('applies alias before exclude before bin in the operation pipeline', () => { const schema: ClassSchema = { classKind: 'biological', columns: [ { index: 0, name: 'Treatment', sourceIndices: [0], uniqueValues: ['Drug-A', 'drug-A', 'CTL', 'CTL'], cardinality: 3, role: 'factor', ops: { alias: { 'drug-A': 'Drug-A' }, exclude: ['CTL'], }, }, ], groupBy: [0], } const result = composeGroups({ tokenizedSamples: [['Drug-A'], ['drug-A'], ['CTL']], sampleNames: ['s1', 's2', 's3'], schemas: { biological: schema }, classes: [ { kind: 'biological', label: 'Biological', members: [0, 1, 2], classTagPositions: [], disposition: 'group', }, ], }) expect(result.experimentalGroups!.length).toBe(1) // Single experimental class → unprefixed post-alias factor value. expect(result.experimentalGroups![0].name).toBe('Drug-A') expect(result.experimentalGroups![0].samples).toEqual(['s1', 's2']) expect(result.excludedSamples).toEqual(['s3']) }) it('stores post-alias / post-bin display values in MetadataRow.fields', () => { const schema: ClassSchema = { classKind: 'biological', columns: [ { index: 0, name: 'Tissue', sourceIndices: [0], uniqueValues: ['Kidney', 'Liver'], cardinality: 2, role: 'factor', ops: { alias: { Kidney: 'kidney_panel' } }, }, ], groupBy: [0], } const result = composeGroups({ tokenizedSamples: [['Kidney']], sampleNames: ['s1'], schemas: { biological: schema }, classes: [ { kind: 'biological', label: 'Biological', members: [0], classTagPositions: [], disposition: 'group', }, ], }) expect(result.metadata[0].fields.Tissue).toBe('kidney_panel') // Single experimental class → unprefixed post-alias value as the group label. expect(result.metadata[0].group).toBe('kidney_panel') }) })