import { resolveToothIntent, resolveToothIntents, } from './tooth-intent.resolver'; import type { ToothIntent } from './voice.types'; const positional = ( arch: 'upper' | 'lower', side: 'patient_right' | 'patient_left', position: number, spoken = 'x', ): ToothIntent => ({ kind: 'positional', arch, side, position, spoken }); const explicit = (fdi: string, spoken = 'x'): ToothIntent => ({ kind: 'explicit', fdi, spoken, }); describe('resolveToothIntent', () => { describe('positional intents', () => { // "شش بالا راست" — upper right six — must be 16, not 26. A mirrored quadrant is a // valid code for the wrong tooth and reaches the lab unnoticed. it('resolves each quadrant from the patient perspective', () => { expect(resolveToothIntent(positional('upper', 'patient_right', 6))).toBe( '16', ); expect(resolveToothIntent(positional('upper', 'patient_left', 6))).toBe( '26', ); expect(resolveToothIntent(positional('lower', 'patient_left', 6))).toBe( '36', ); expect(resolveToothIntent(positional('lower', 'patient_right', 6))).toBe( '46', ); }); it('returns null for an out-of-range position instead of clamping', () => { expect( resolveToothIntent(positional('upper', 'patient_right', 9)), ).toBeNull(); expect( resolveToothIntent(positional('upper', 'patient_right', 0)), ).toBeNull(); }); it('returns null for a malformed arch or side', () => { expect( resolveToothIntent( positional('sideways' as 'upper', 'patient_right', 6), ), ).toBeNull(); expect( resolveToothIntent( positional('upper', 'viewer_right' as 'patient_right', 6), ), ).toBeNull(); }); }); describe('explicit intents', () => { it('accepts a permanent FDI code', () => { expect(resolveToothIntent(explicit('14'))).toBe('14'); expect(resolveToothIntent(explicit('48'))).toBe('48'); }); it('rejects deciduous codes rather than snapping to a permanent tooth', () => { expect(resolveToothIntent(explicit('51'))).toBeNull(); expect(resolveToothIntent(explicit('85'))).toBeNull(); }); it('rejects nonsense', () => { for (const value of ['', '1', '99', '140']) { expect(resolveToothIntent(explicit(value))).toBeNull(); } }); }); it('returns null for a missing or unknown intent shape', () => { expect(resolveToothIntent(undefined as unknown as ToothIntent)).toBeNull(); expect( resolveToothIntent({ kind: 'guess' } as unknown as ToothIntent), ).toBeNull(); }); }); describe('resolveToothIntents', () => { it('resolves a mixed list and sorts the result', () => { const result = resolveToothIntents([ positional('upper', 'patient_right', 5, 'پنج بالا راست'), explicit('14', 'یک چهار'), ]); expect(result.teeth).toEqual(['14', '15']); expect(result.unresolved).toEqual([]); }); it('collapses a tooth named twice', () => { const result = resolveToothIntents([ explicit('14', 'چهارده'), positional('upper', 'patient_right', 4, 'چهار بالا راست'), ]); expect(result.teeth).toEqual(['14']); }); it('reports what it could not understand instead of dropping it', () => { const result = resolveToothIntents([ explicit('14', 'یک چهار'), explicit('51', 'دندان شیری'), positional('upper', 'patient_right', 9, 'نه بالا راست'), ]); expect(result.teeth).toEqual(['14']); expect(result.unresolved).toEqual([ { spoken: 'دندان شیری', reason: 'not_permanent_tooth' }, { spoken: 'نه بالا راست', reason: 'position_out_of_range' }, ]); }); it('does not repeat an identical unresolved item', () => { const result = resolveToothIntents([ explicit('51', 'شیری'), explicit('51', 'شیری'), ]); expect(result.unresolved).toHaveLength(1); }); it('survives a non-array where the model should have sent a list', () => { // The model can return an object or a number here; that must degrade, not 500. for (const bad of [undefined, null, 5, 'teeth', { fdi: '14' }]) { expect(resolveToothIntents(bad as unknown as ToothIntent[])).toEqual({ teeth: [], unresolved: [], }); } }); it('trims an explicit code, matching normalizeTeeth', () => { expect(resolveToothIntents([explicit(' 14 ', 'x')]).teeth).toEqual(['14']); }); it('distinguishes a deciduous tooth from nonsense in the reason it reports', () => { // '51' really is a (primary) tooth the chart cannot show; '99' is not a tooth at all. expect( resolveToothIntents([explicit('51', 'shiri')]).unresolved[0].reason, ).toBe('not_permanent_tooth'); for (const junk of ['99', '19', '140', '', 'ab']) { expect( resolveToothIntents([explicit(junk, `j-${junk}`)]).unresolved[0].reason, ).toBe('malformed'); } }); it('keeps unresolved items separate when the model omits the spoken span', () => { // Without `spoken` these are indistinguishable; collapsing them would hide a lost tooth. const result = resolveToothIntents([ { kind: 'explicit', fdi: '51' } as ToothIntent, { kind: 'explicit', fdi: '52' } as ToothIntent, ]); expect(result.unresolved).toHaveLength(2); }); it('handles an empty or missing list', () => { expect(resolveToothIntents([])).toEqual({ teeth: [], unresolved: [] }); expect(resolveToothIntents(undefined as unknown as ToothIntent[])).toEqual({ teeth: [], unresolved: [], }); }); });