import { randomUUID } from "node:crypto"; import { beforeEach, describe, expect, it } from "vitest"; import { deviceEvents as deviceEventsTable, ledgerEvents, sessions, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import { flagDuplicateEntryPlate } from "./snapshot.js"; import { makeLog, silentLogger } from "./test-helpers.js"; import type { EventLog } from "./event-log.js"; // Entry-side duplicate-plate reconciliation (2026-07-04): when ANPR recognizes a plate on // a fresh transient entry and that plate is already OPEN under another RECENT session, // the same car most likely minted a second ticket (a motion radar dropped the stationary // car → the button re-armed). We sign ONE entry.duplicatePlate anomaly for the operator // to void. Post-hoc + advisory: recognition never gates the (already-open) barrier — // exactly the non-blocking role the plate can play here. let db: Db; let log: EventLog; const PLATE = "AA111BB"; const OLD = "11111111111"; const NEW = "22222222222"; beforeEach(() => { ({ db } = createTestDb()); log = makeLog(db); }); /** Seed the prior entry's unsigned plate-read telemetry (what recognizePlate records). */ function seedPriorRead(opts: { identity?: string; plate?: string; direction?: string; agoMs?: number } = {}) { db.insert(deviceEventsTable).values({ id: randomUUID(), deviceId: "cam-entry", category: "camera", kind: "read", detail: { identity: opts.identity ?? OLD, direction: opts.direction ?? "entry", plate: opts.plate ?? PLATE, snapshotId: "snap-old", source: "entry-exit-snapshot", }, occurredAt: new Date(Date.now() - (opts.agoMs ?? 60_000)).toISOString(), }).run(); } function seedSession(id: string, state: "open" | "closed") { db.insert(sessions).values({ id, identity: id, source: "ticket", enteredAt: new Date(Date.now() - 60_000).toISOString(), state, }).run(); } const flag = () => flagDuplicateEntryPlate({ db, log, identity: NEW, plate: PLATE, snapshotId: "snap-new", logger: silentLogger() }); const anomalies = () => db.select().from(ledgerEvents).all().filter((r) => r.type === "anomaly"); describe("flagDuplicateEntryPlate", () => { it("same plate OPEN under another recent session → signs ONE entry.duplicatePlate anomaly", async () => { seedPriorRead(); seedSession(OLD, "open"); await flag(); expect(anomalies()).toHaveLength(1); const a = anomalies()[0]; expect(a.identity).toBe(NEW); // keyed to the NEW (suspect) ticket expect(a.payload).toMatchObject({ reasonCode: "entry.duplicatePlate", duplicateEntrySuspected: true, plate: PLATE, otherIdentity: OLD, snapshotId: "snap-new", }); }); it("prior session already CLOSED → no anomaly (that car drove off; a re-visit is legit)", async () => { seedPriorRead(); seedSession(OLD, "closed"); await flag(); expect(anomalies()).toHaveLength(0); }); it("prior read outside the window → no anomaly (stale coincidence, not a double press)", async () => { seedPriorRead({ agoMs: 30 * 60_000 }); // beyond the 15-min default window seedSession(OLD, "open"); await flag(); expect(anomalies()).toHaveLength(0); }); it("own read (same identity) never flags itself", async () => { seedPriorRead({ identity: NEW }); seedSession(NEW, "open"); await flag(); expect(anomalies()).toHaveLength(0); }); it("different plate / exit-side reads are ignored", async () => { seedPriorRead({ plate: "ZZ999ZZ" }); seedPriorRead({ direction: "exit" }); seedSession(OLD, "open"); await flag(); expect(anomalies()).toHaveLength(0); }); });