import { describe, expect, it } from 'vitest'; import { buildOcrMetaFromIdentifyResult, buildScannedCardPayload, candidateMatchesSetHint, resolveDisambiguationRefineAction, resolveIdentifyOutcome, } from '../../lib/scanner-card-identify.js'; describe('candidateMatchesSetHint', () => { it('matches when set code equals hint', () => { expect(candidateMatchesSetHint({ set_name: 'Base Set', set_code: 'BS' }, null, 'BS')).toBe(true); }); it('returns true when no hint is provided', () => { expect(candidateMatchesSetHint({ set_name: 'Any Set' }, null, null)).toBe(true); }); it('returns false for unrelated sets', () => { expect(candidateMatchesSetHint({ set_name: 'Jungle', set_code: 'JU' }, 'Base Set', null)).toBe(false); }); }); describe('resolveIdentifyOutcome', () => { it('returns emit when a single card is matched', () => { const card = { id: 1, name: 'Pikachu', mana_cost: '1R' }; const outcome = resolveIdentifyOutcome({ isCard: true, card, ocr: { confidence: 90, rawText: 'Pikachu' }, }); expect(outcome).toMatchObject({ type: 'emit', cardStatus: 'confirmed', card, }); expect(outcome.ocrMeta.confidence).toBe(90); }); it('returns disambiguation when multiple printings match', () => { const matches = [{ id: 1, name: 'Lightning Bolt' }, { id: 2, name: 'Lightning Bolt' }]; const outcome = resolveIdentifyOutcome({ isCard: true, needsUserSelection: true, matches, message: 'Pick one', }); expect(outcome).toEqual({ type: 'disambiguation', cardStatus: 'confirmed', matches, ocrMeta: expect.objectContaining({ abilities: [] }), message: 'Pick one', fromLayer1: false, fromLayer0: false, }); }); it('returns error when the frame is not a card', () => { expect(resolveIdentifyOutcome({ isCard: false, reason: 'Blurry' })).toEqual({ type: 'error', cardStatus: 'negative', message: 'Blurry', }); }); it('marks L1 disambiguation so Gemini refine is skipped', () => { const outcome = resolveIdentifyOutcome({ isCard: true, layer: 1, needsUserSelection: true, matches: [{ id: 1, name: 'Bolt' }], message: 'Pick one', }); expect(outcome.fromLayer1).toBe(true); }); }); describe('buildScannedCardPayload', () => { it('maps catalog fields and OCR metadata into the scanner queue shape', () => { const payload = buildScannedCardPayload( { id: 42, name: 'Seel', set_name: 'Perfect Order', set_code: 'PO', card_number: '015/208', game: 'pokemon', card_type: 'Pokemon', rarity: 'Common', image_url: 'https://example.com/seel.jpg', }, { imageData: 'data:image/jpeg;base64,abc', scanImageUrl: 'https://blob.example/seel.jpg', ocrMeta: { rawText: 'Seel', confidence: 88, abilities: ['Freeze-Dry'] }, } ); expect(payload).toMatchObject({ name: 'Seel', set: 'Perfect Order', databaseId: 42, scanImageUrl: 'https://blob.example/seel.jpg', ocrText: 'Seel', confidence: 88, abilities: ['Freeze-Dry'], }); }); }); describe('resolveDisambiguationRefineAction', () => { const candidates = [ { id: 1, name: 'Bolt', set_name: 'Alpha', set_code: 'LEA' }, { id: 2, name: 'Bolt', set_name: 'Beta', set_code: 'LEB' }, ]; it('auto-picks when vision narrows to one printing', () => { const action = resolveDisambiguationRefineAction( { matches: [{ id: 1 }], ocr: { setCode: 'LEA' }, }, { candidates } ); expect(action).toEqual({ type: 'pick', candidate: candidates[0] }); }); it('requests admin review path when set hint misses the catalog', () => { const action = resolveDisambiguationRefineAction( { matches: [{}], ocr: { setName: 'Unknown Set', cardName: 'Bolt' }, }, { candidates } ); expect(action.type).toBe('catalog_gap'); expect(action.submitPayload.candidateCardIds).toEqual([1, 2]); }); }); describe('buildOcrMetaFromIdentifyResult', () => { it('prefers top-level OCR fields over nested card OCR', () => { expect( buildOcrMetaFromIdentifyResult({ ocr: { confidence: 95, rawText: 'From top' }, card: { ocr: { confidence: 50, rawText: 'From card', abilities: ['Flying'] }, hp: 4 }, }) ).toMatchObject({ confidence: 95, rawText: 'From top', abilities: ['Flying'], hp: 4, }); }); });