import { describe, expect, it } from "vitest"; import { detailFingerprint, fingerprintDataUrl, isRepeatedProcessedPageFingerprint, screenFingerprint } from "./autoScanLoop"; import { runAutoScanLoop } from "./autoScanLoop"; import type { AutoScanLoopDependencies } from "./autoScanLoop"; import type { CaptureResult } from "../types/global"; type ScanTestCapture = CaptureResult & { inventoryGrid: NonNullable }; const sampleGrid = { centers: [{ x: 80, y: 90, row: 0, col: 0 }], rows: 1, cols: 1, confidence: 86, source: "detected" as const, }; const sampleParse = { name: "A Tiara of Torrents", slot: "Flower of Life", level: 16, mainStat: "HP", mainValue: "10.7%", substats: ["ATK+29", "DEF+10"], setName: "Tenacity of the Millelith", equipped: "Traveler", confidence: 84, notes: [], fields: { name: { value: "A Tiara of Torrents", confidence: 84, source: "ocr" as const }, slot: { value: "Flower of Life", confidence: 84, source: "ocr" as const }, level: { value: "16", confidence: 84, source: "ocr" as const }, mainStat: { value: "HP", confidence: 84, source: "ocr" as const }, mainValue: { value: "10.7%", confidence: 84, source: "ocr" as const }, setName: { value: "Tenacity of the Millelith", confidence: 84, source: "ocr" as const }, equipped: { value: "Traveler", confidence: 84, source: "ocr" as const }, substats: { value: "ATK+29, DEF+10", confidence: 84, source: "ocr" as const }, }, }; function capture(overrides: Partial = {}): ScanTestCapture { return { id: "source", name: "screen-capture", width: 1920, height: 1080, dataUrl: "data:image/png;base64,AA", capturedAt: "2026-01-01T00:00:00.000Z", captureTarget: "desktop-source", ocr: [], detailDataUrl: "data:image/png;base64,DETAIL-AAA", inventoryDataUrl: "data:image/png;base64,GRID-AAA", inventoryGrid: sampleGrid, ...overrides, }; } describe("autoScanLoop fingerprints", () => { it("distinguishes captures that share the same prefix but differ later", () => { const sharedPrefix = "data:image/png;base64," + "A".repeat(180); const left = sharedPrefix + "LEFT-" + "B".repeat(600); const right = sharedPrefix + "RIGHT-" + "C".repeat(600); expect(fingerprintDataUrl(left)).not.toBe(fingerprintDataUrl(right)); }); it("stays stable for the same capture string", () => { const dataUrl = "data:image/png;base64," + "Q".repeat(2048); expect(fingerprintDataUrl(dataUrl)).toBe(fingerprintDataUrl(dataUrl)); }); it("uses the detail preview for detail-change verification instead of the full frame", () => { const captureA = { detailDataUrl: "data:image/png;base64," + "DETAIL-A".repeat(64), dataUrl: "data:image/png;base64," + "FULL-A".repeat(256), } as const; const captureB = { detailDataUrl: captureA.detailDataUrl, dataUrl: "data:image/png;base64," + "FULL-B".repeat(256), } as const; expect(detailFingerprint(captureA as never)).toBe(detailFingerprint(captureB as never)); }); it("uses the inventory preview for scroll verification when available", () => { const captureA = { inventoryDataUrl: "data:image/png;base64," + "GRID-A".repeat(64), dataUrl: "data:image/png;base64," + "FRAME-A".repeat(256), } as const; const captureB = { inventoryDataUrl: "data:image/png;base64," + "GRID-B".repeat(64), dataUrl: captureA.dataUrl, } as const; expect(screenFingerprint(captureA as never)).not.toBe(screenFingerprint(captureB as never)); }); it("treats repeated page fingerprints as a loop only after page one", () => { const seen = new Set(["abc"]); expect(isRepeatedProcessedPageFingerprint("abc", seen, 1)).toBe(false); expect(isRepeatedProcessedPageFingerprint("abc", seen, 2)).toBe(true); expect(isRepeatedProcessedPageFingerprint("", seen, 3)).toBe(false); }); it("does not block before first click when start capture is from the primary screen", async () => { const startCapture = capture({ captureTarget: "primary-screen", detailDataUrl: `data:image/png;base64,${"D".repeat(600)}`, }); let clicked = 0; const deps: AutoScanLoopDependencies = { api: { clickScreen: async () => { clicked += 1; return { ok: true, x: 0, y: 0, cursorX: 0, cursorY: 0, clicked: true, moved: true, focused: true, }; }, scrollScreen: async () => ({ ok: true, notchesSent: 0 }), getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }), }, captureSelectedSource: async () => capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` }), captureFastSelectedSource: async () => startCapture, parseArtifact: () => sampleParse, persistParsedArtifact: async () => false, saveReviewSample: async () => ({ ok: true }), getAutoReviewReason: () => "", shouldFlagArtifactForReview: () => false, appendAutomationLog: () => undefined, appendClickDiagnostics: () => undefined, setReviewStatus: () => undefined, setAutoScanStats: () => undefined, shouldStop: () => false, }; const result = await runAutoScanLoop(deps, { scanLimit: 1, skipRows: 0, detectedInventoryCount: null }); expect(result.blockedReason).toBe(""); expect(result.status).toBe("done"); expect(clicked).toBe(1); }); });