deckhearth/test/lib/scanner-card-detection.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

127 lines
3.6 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import {
convertToVideoCoordinates,
mergeDetectedShapesIntoTrackedCards,
rectanglesOverlap,
selectCardsReadyForVerification,
TRACK_STALE_MS,
} from '../../lib/scanner-card-detection.js';
describe('rectanglesOverlap', () => {
it('returns true when rectangles mostly overlap', () => {
const a = { x: 0, y: 0, width: 100, height: 140 };
const b = { x: 10, y: 10, width: 100, height: 140 };
expect(rectanglesOverlap(a, b, 0.3)).toBe(true);
});
it('returns false when rectangles do not touch', () => {
const a = { x: 0, y: 0, width: 50, height: 70 };
const b = { x: 200, y: 200, width: 50, height: 70 };
expect(rectanglesOverlap(a, b)).toBe(false);
});
});
describe('convertToVideoCoordinates', () => {
it('scales detection canvas coords to video pixel space', () => {
const canvas = { width: 320, height: 240 };
const video = { videoWidth: 1280, videoHeight: 960 };
expect(convertToVideoCoordinates({ x: 10, y: 20, width: 100, height: 140 }, canvas, video)).toEqual({
x: 40,
y: 80,
width: 400,
height: 560,
});
});
});
describe('mergeDetectedShapesIntoTrackedCards', () => {
it('creates a new tracked card for an unmatched shape', () => {
const now = 1_000_000;
const shape = { x: 10, y: 20, width: 100, height: 140, score: 80 };
const { cards, nextCardId } = mergeDetectedShapesIntoTrackedCards([], [shape], 1, now);
expect(cards).toHaveLength(1);
expect(cards[0]).toMatchObject({
id: 1,
status: 'detecting',
stableCount: 1,
scanAttempts: 0,
bounds: shape,
});
expect(nextCardId).toBe(2);
});
it('updates an overlapping tracked card instead of creating a duplicate', () => {
const now = 1_000_000;
const existing = {
id: 5,
bounds: { x: 10, y: 20, width: 100, height: 140 },
status: 'detecting',
firstSeen: now - 500,
lastSeen: now - 100,
stableCount: 3,
scanAttempts: 0,
};
const moved = { x: 12, y: 22, width: 100, height: 140, score: 82 };
const { cards, nextCardId } = mergeDetectedShapesIntoTrackedCards([existing], [moved], 6, now);
expect(cards).toHaveLength(1);
expect(cards[0].id).toBe(5);
expect(cards[0].stableCount).toBe(4);
expect(cards[0].lastSeen).toBe(now);
expect(nextCardId).toBe(6);
});
it('drops cards not seen within TRACK_STALE_MS', () => {
const now = 10_000;
const stale = {
id: 1,
bounds: { x: 0, y: 0, width: 50, height: 70 },
status: 'detecting',
firstSeen: now - TRACK_STALE_MS - 1,
lastSeen: now - TRACK_STALE_MS - 1,
stableCount: 2,
scanAttempts: 0,
};
const { cards } = mergeDetectedShapesIntoTrackedCards([stale], [], 2, now);
expect(cards).toHaveLength(0);
});
});
describe('selectCardsReadyForVerification', () => {
it('returns detecting cards that are stable long enough', () => {
const now = 10_000;
const ready = {
id: 1,
status: 'detecting',
stableCount: 3,
scanAttempts: 0,
firstSeen: now - 900,
};
const tooFresh = {
id: 2,
status: 'detecting',
stableCount: 3,
scanAttempts: 0,
firstSeen: now - 500,
};
expect(selectCardsReadyForVerification([ready, tooFresh], now)).toEqual([ready]);
});
it('excludes cards that already attempted verification', () => {
const now = 10_000;
const retried = {
id: 1,
status: 'detecting',
stableCount: 8,
scanAttempts: 1,
firstSeen: now - 5000,
};
expect(selectCardsReadyForVerification([retried], now)).toHaveLength(0);
});
});