import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { randomUUID } from "node:crypto"; import { devices, deviceEvents as deviceEventsTable, eq, siteConfig, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import { deviceEvents, type DeviceReadEvent } from "./device-events.js"; import { silentLogger } from "./test-helpers.js"; import type { VisionClient, VisionResult } from "./vision-client.js"; import type { SubscriptionFlow, SubscriptionMatch } from "./subscription-flow.js"; // The ANPR bridge: a camera vehicle detection → (opt-in) snapshot → plate → MATCH a // subscriber → emit a plate read. We mock the camera build (buildCamera) so no real // snapshot HTTP is made, and pass fake Vision/Subscription so the test is the bridge's // own logic only. See anpr-entry.ts. // Mock buildCamera so the bridge gets a fake camera whose captureSnapshot is a stub // (no registry, no network). The factory returns a fresh shot each call. const captureSnapshot = vi.fn(async () => ({ bytes: Buffer.from("jpg"), contentType: "image/jpeg" })); vi.mock("./snapshot.js", () => ({ buildCamera: () => ({ captureSnapshot }), })); // Import AFTER the mock is registered. const { AnprBridge } = await import("./anpr-entry.js"); let db: Db; beforeEach(() => { ({ db } = createTestDb()); captureSnapshot.mockClear(); delete process.env.VISION_ENTRY_MIN_CONFIDENCE; delete process.env.ANPR_DEBOUNCE_MS; }); afterEach(() => { vi.restoreAllMocks(); }); /** A camera bound to an entry relay; `anpr` toggles the opt-in flag. */ function seedCamera(opts: { anpr?: boolean } = {}): string { const controllerId = randomUUID(); db.insert(devices).values({ id: controllerId, category: "access", driverId: "dingtian", config: { host: "10.0.0.5", relays: [{ relay: 1, direction: "entry" }] }, enabled: true, }).run(); const camId = randomUUID(); db.insert(devices).values({ id: camId, category: "camera", driverId: "hikvision", config: { host: "10.0.0.9", controllerId, relay: 1, ...(opts.anpr ? { anpr: true } : {}) }, enabled: true, }).run(); return camId; } /** A fake VisionClient: enabled, returning a chosen plate/confidence (or null). */ function fakeVision(opts: { enabled?: boolean; plate?: string; confidence?: number } = {}): VisionClient { const enabled = opts.enabled ?? true; const result: VisionResult | null = opts.plate == null ? null : { plate: { text: opts.plate, confidence: opts.confidence ?? 0.99 }, plates: [], lowConfidence: false, modelVersion: "test", tookMs: 1, }; return { enabled, analyze: vi.fn(async () => (enabled ? result : null)), } as unknown as VisionClient; } /** A fake SubscriptionFlow: only `match()` is called by the bridge. */ function fakeSubFlow(match: SubscriptionMatch | null): SubscriptionFlow { return { match: vi.fn(() => match) } as unknown as SubscriptionFlow; } const SUB_MATCH: SubscriptionMatch = { subscriptionId: "sub-1", carKey: "AA111BB", via: "plate" }; /** Capture read events emitted during `fn` (async). */ async function captureReads(fn: () => Promise): Promise { const got: DeviceReadEvent[] = []; const off = deviceEvents.onRead((e) => got.push(e)); try { await fn(); } finally { off(); } return got; } describe("AnprBridge", () => { it("does nothing for an opt-OUT camera (no anpr flag) — no analyze, no read", async () => { const cam = seedCamera({ anpr: false }); const vision = fakeVision({ plate: "AA111BB" }); const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toEqual([]); expect(vision.analyze).not.toHaveBeenCalled(); expect(captureSnapshot).not.toHaveBeenCalled(); }); it("emits a plate read (upper-cased) for a high-confidence SUBSCRIBER plate", async () => { const cam = seedCamera({ anpr: true }); const vision = fakeVision({ plate: " aa111bb ", confidence: 0.97 }); const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toHaveLength(1); expect(reads[0]).toMatchObject({ deviceId: cam, value: "AA111BB", kind: "plate", driverId: "hikvision" }); }); it("ignores a plate below the entry confidence floor", async () => { const cam = seedCamera({ anpr: true }); const vision = fakeVision({ plate: "AA111BB", confidence: 0.6 }); // < default 0.85 const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toEqual([]); }); it("does NOT emit for a plate matching no subscription — records an advisory anpr-skip", async () => { const cam = seedCamera({ anpr: true }); const vision = fakeVision({ plate: "ZZ999ZZ", confidence: 0.97 }); const bridge = new AnprBridge(db, vision, fakeSubFlow(null), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toEqual([]); const skips = db.select().from(deviceEventsTable).where(eq(deviceEventsTable.kind, "anpr-skip")).all(); expect(skips).toHaveLength(1); expect((skips[0].detail as { plate?: string }).plate).toBe("ZZ999ZZ"); }); it("debounces: two vehicle events within the window analyze/emit at most once", async () => { const cam = seedCamera({ anpr: true }); const vision = fakeVision({ plate: "AA111BB", confidence: 0.97 }); const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(async () => { await bridge.onVehicleDetected(cam); await bridge.onVehicleDetected(cam); // within the 12s window → suppressed }); expect(reads).toHaveLength(1); expect(captureSnapshot).toHaveBeenCalledTimes(1); // 2nd was gated before the snapshot }); it("is a no-op (no throw) when vision is disabled or reads nothing", async () => { const cam = seedCamera({ anpr: true }); const disabled = new AnprBridge(db, fakeVision({ enabled: false, plate: "AA111BB" }), fakeSubFlow(SUB_MATCH), silentLogger()); const noPlate = new AnprBridge(db, fakeVision({ plate: undefined }), fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(async () => { await disabled.onVehicleDetected(cam); await noPlate.onVehicleDetected(cam); }); expect(reads).toEqual([]); }); it("never throws on an unknown device id", async () => { const bridge = new AnprBridge(db, fakeVision({ plate: "AA111BB" }), fakeSubFlow(SUB_MATCH), silentLogger()); await expect(bridge.onVehicleDetected("nope")).resolves.toBeUndefined(); }); it("does NOTHING when the admin has disabled the bridge (site_config.anprEntryEnabled = false)", async () => { const cam = seedCamera({ anpr: true }); db.insert(siteConfig).values({ id: 1, anprEntryEnabled: false }).run(); const vision = fakeVision({ plate: "AA111BB", confidence: 0.97 }); const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toEqual([]); // The flag is checked FIRST — no snapshot, no analyze, no match attempt. expect(captureSnapshot).not.toHaveBeenCalled(); expect(vision.analyze).not.toHaveBeenCalled(); }); it("still emits when the bridge is explicitly enabled (anprEntryEnabled = true)", async () => { const cam = seedCamera({ anpr: true }); db.insert(siteConfig).values({ id: 1, anprEntryEnabled: true }).run(); const vision = fakeVision({ plate: "AA111BB", confidence: 0.97 }); const bridge = new AnprBridge(db, vision, fakeSubFlow(SUB_MATCH), silentLogger()); const reads = await captureReads(() => bridge.onVehicleDetected(cam)); expect(reads).toHaveLength(1); }); });