import { beforeEach, describe, expect, it } from "vitest"; import { eq, devices, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import { LanePresence } from "./lane-presence.js"; import { deviceEvents, type DeviceInputEvent, type LanePresenceEvent } from "./device-events.js"; import { silentLogger } from "./test-helpers.js"; // LanePresence: a vehicle-presence INPUT edge (loop/radar) on an entry/exit barrier marks // that lane "present" — the same signal that blinks the physical button lamp (relay 3). It // resolves the edge via relayForPresence (the SAME path relay 3 + the entry gate use), and // emits a lane-presence change only when a lane's present/clear state actually flips. let db: Db; const CTL = "ctl-1"; const ENTRY_RADAR = 2; const EXIT_RADAR = 5; beforeEach(() => { ({ db } = createTestDb()); // Entry relay 1 with a radar on I2; exit relay 2 with a radar on I5. db.insert(devices).values({ id: CTL, category: "access", driverId: "dingtian", config: { host: "10.0.0.5", relays: [ { relay: 1, direction: "entry" }, { relay: 2, direction: "exit" }, ], inputs: [ { input: ENTRY_RADAR, role: "presence", relay: 1, kind: "radar" }, { input: EXIT_RADAR, role: "presence", relay: 2, kind: "radar" }, ], }, enabled: true, }).run(); }); function edge(input: number, on: boolean): void { const e: DeviceInputEvent = { driverId: "dingtian", deviceId: CTL, input, edge: on ? "on" : "off", at: new Date().toISOString(), source: "poll", }; deviceEvents.emitInput(e); } /** Collect lane-presence emissions while running `fn`. */ function capture(fn: () => void): LanePresenceEvent[] { const seen: LanePresenceEvent[] = []; const off = deviceEvents.onLanePresence((p) => seen.push(p)); try { fn(); } finally { off(); } return seen; } describe("LanePresence", () => { it("starts clear and snapshots clear", () => { const lp = new LanePresence(db, silentLogger()); lp.start(); expect(lp.snapshot()).toEqual({ entry: false, exit: false }); lp.stop(); }); it("an ENTRY radar edge marks the entry lane present, then clears", () => { const lp = new LanePresence(db, silentLogger()); lp.start(); const events = capture(() => { edge(ENTRY_RADAR, true); edge(ENTRY_RADAR, false); }); expect(events).toEqual([ { entry: true, exit: false }, { entry: false, exit: false }, ]); lp.stop(); }); it("an EXIT radar edge marks the exit lane independently", () => { const lp = new LanePresence(db, silentLogger()); lp.start(); const events = capture(() => { edge(EXIT_RADAR, true); }); expect(events).toEqual([{ entry: false, exit: true }]); expect(lp.snapshot()).toEqual({ entry: false, exit: true }); lp.stop(); }); it("de-dupes: a second 'on' from another presence input on the same lane emits once", () => { // Two radars both serving the entry lane. db.update(devices) .set({ config: { host: "10.0.0.5", relays: [{ relay: 1, direction: "entry" }], inputs: [ { input: 2, role: "presence", relay: 1, kind: "radar" }, { input: 3, role: "presence", relay: 1, kind: "radar" }, ], }, }) .where(eq(devices.id, CTL)) .run(); const lp = new LanePresence(db, silentLogger()); lp.start(); const events = capture(() => { edge(2, true); // entry → present (emit) edge(3, true); // still present (no emit — same lane) edge(2, false); // still present via I3 (no emit) edge(3, false); // now clear (emit) }); expect(events).toEqual([ { entry: true, exit: false }, { entry: false, exit: false }, ]); lp.stop(); }); it("ignores a non-presence input (e.g. a button terminal)", () => { db.update(devices) .set({ config: { host: "10.0.0.5", relays: [{ relay: 1, direction: "entry" }], inputs: [{ input: 1, role: "button", relay: 1 }], }, }) .where(eq(devices.id, CTL)) .run(); const lp = new LanePresence(db, silentLogger()); lp.start(); const events = capture(() => edge(1, true)); expect(events).toEqual([]); lp.stop(); }); });