import { beforeEach, describe, expect, it } from "vitest"; import { devices, siteConfig, ledgerEvents, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import { EntryFlow } from "./entry-flow.js"; import { makeLog, silentLogger } from "./test-helpers.js"; // The entry presence gate normally requires BOTH radar/loop presence AND camera detection. // An admin may BYPASS a signal when its device is faulty (site_config, set via a signed // endpoint). These tests pin the GATE decision in EntryFlow.issueForOperator under each // bypass combination: a still-required-but-absent signal refuses (+ signs an anomaly); a // bypassed signal is dropped and recorded. We assert the gate outcome via the refuse path // (deterministic, no printer needed); the allow path is proven by getting PAST the gate // (it then fails at printing — a different reason — which is exactly "the gate opened"). let db: Db; let flow: EntryFlow; const CTL = "ctl-entry"; const PRESENCE_INPUT = 2; beforeEach(() => { ({ db } = createTestDb()); // A controller with an entry barrier (R1), a presence loop on input 2, and an entry button // on input 1 — the shape device-resolve expects (relays[] + inputs[]). db.insert(devices).values({ id: CTL, category: "access", driverId: "stub-access", config: { relays: [{ relay: 1, direction: "entry" }], inputs: [ { input: 1, role: "button", relay: 1 }, { input: PRESENCE_INPUT, role: "presence", relay: 1, kind: "loop" }, ], }, enabled: true, }).run(); flow = new EntryFlow(db, makeLog(db), silentLogger()); }); function setBypass(patch: { radar?: boolean; camera?: boolean }) { db.insert(siteConfig) .values({ id: 1, bypassPresenceRadar: patch.radar ?? false, bypassPresenceCamera: patch.camera ?? false }) .onConflictDoUpdate({ target: siteConfig.id, set: { bypassPresenceRadar: patch.radar ?? false, bypassPresenceCamera: patch.camera ?? false }, }) .run(); } /** Drive a presence loop edge so the flow's per-relay guard marks a car present/clear. */ async function setRadarPresent(present: boolean) { await flow.onInput({ driverId: "stub-access", deviceId: CTL, input: PRESENCE_INPUT, edge: present ? "on" : "off", at: new Date().toISOString(), source: "poll", }); } const anomalies = () => db.select().from(ledgerEvents).all().filter((r) => r.type === "anomaly"); describe("entry presence-gate bypass", () => { it("no bypass + no vehicle → refuses and signs a noPresence anomaly", async () => { const res = await flow.issueForOperator("admin", /*cameraBusy*/ false); expect(res.ok).toBe(false); expect(anomalies()).toHaveLength(1); expect(anomalies()[0].payload).toMatchObject({ reasonCode: "entry.issue.noPresence" }); }); it("camera bypassed + radar present → gate OPENS (no refuse anomaly)", async () => { setBypass({ camera: true }); await setRadarPresent(true); const res = await flow.issueForOperator("admin", /*cameraBusy*/ false); // camera absent but bypassed // Gate passed: no noPresence refusal. (It then proceeds to print — no printer configured, // so it HOLDS with a print reason, not a presence reason. Either way the gate opened.) const refusals = anomalies().filter((a) => (a.payload as { reasonCode?: string }).reasonCode === "entry.issue.noPresence"); expect(refusals).toHaveLength(0); if (!res.ok) expect(res.reason).not.toMatch(/no vehicle detected/); }); it("radar bypassed + camera busy → gate OPENS even with NO presence loop reading", async () => { setBypass({ radar: true }); // radar NOT set present; camera busy=true → radar dropped, camera satisfies. const res = await flow.issueForOperator("admin", /*cameraBusy*/ true); const refusals = anomalies().filter((a) => (a.payload as { reasonCode?: string }).reasonCode === "entry.issue.noPresence"); expect(refusals).toHaveLength(0); if (!res.ok) expect(res.reason).not.toMatch(/no vehicle detected/); }); it("camera bypassed but radar STILL required and absent → refuses (only the faulty signal is dropped)", async () => { setBypass({ camera: true }); await setRadarPresent(false); // radar required (not bypassed) and clear const res = await flow.issueForOperator("admin", /*cameraBusy*/ true); expect(res.ok).toBe(false); const refusal = anomalies().find((a) => (a.payload as { reasonCode?: string }).reasonCode === "entry.issue.noPresence"); expect(refusal, "the still-required radar gates the button").toBeTruthy(); // The refusal records which signal was bypassed (audit). expect(refusal!.payload).toMatchObject({ presenceBypassed: ["camera"] }); }); it("both bypassed → gate OPENS with no radar and no camera (press-to-print)", async () => { setBypass({ radar: true, camera: true }); const res = await flow.issueForOperator("admin", /*cameraBusy*/ false); const refusals = anomalies().filter((a) => (a.payload as { reasonCode?: string }).reasonCode === "entry.issue.noPresence"); expect(refusals).toHaveLength(0); if (!res.ok) expect(res.reason).not.toMatch(/no vehicle detected/); }); });