deckhearth/test/lib/scanner-card-identify.test.js

162 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

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,
});
});
});