deckhearth/test/lib/scanner-card-identify.test.js
varutasu 0b4f419f49
Scanner identify upgrade — Phase 1 hot path (#156)
* docs(convoy): seed scanner identify upgrade epic and sub-convoys

Baseline scan_attempts telemetry and three-phase plan for faster, more
accurate card identification without touching scanner chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(scanner): tighten Layer-1 identify hot path (Phase 1)

Cut verify hold-still gates, OCR collector numbers on Layer 1, request
structured Gemini JSON, and skip automatic L2 refine when L1 opens the
printing picker. Includes convoy UX/architecture briefs and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 20:21:20 -05:00

160 lines
4.5 KiB
JavaScript

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