import { beforeEach, describe, expect, it } from "vitest"; import { devices, type Db } from "@parking/db"; import { createTestDb } from "@parking/db/testing"; import { inputsOf, relayForButton, relayForPresence } from "./device-resolve.js"; // device-resolve: the input resolution layer. Inputs live in config.inputs[] (the first-class // model); a pre-inputs[] controller is back-compat-synthesized from the legacy per-relay // button/presenceInput fields. relayForButton/relayForPresence must resolve IDENTICALLY from // either shape, so an exit radar = just another presence row. let db: Db; const CTL = "ctl-1"; function seed(config: Record): void { ({ db } = createTestDb()); db.insert(devices).values({ id: CTL, category: "access", driverId: "dingtian", config, enabled: true }).run(); } describe("inputsOf back-compat synth", () => { it("synthesizes inputs[] from legacy relay button/presence fields", () => { seed({ relays: [ { relay: 1, direction: "entry", button: 1, presenceInput: 2, presenceKind: "radar", presenceActiveLow: true }, { relay: 2, direction: "exit" }, ], }); const row = db.select().from(devices).get()!; const inputs = inputsOf(row); expect(inputs).toEqual([ { input: 1, role: "button", relay: 1, cooldownSec: undefined }, { input: 2, role: "presence", relay: 1, kind: "radar", activeLow: true }, ]); }); it("prefers an explicit inputs[] over the legacy fields", () => { seed({ relays: [{ relay: 1, direction: "entry", button: 9 /* legacy ignored */ }], inputs: [{ input: 1, role: "button", relay: 1 }], }); const row = db.select().from(devices).get()!; expect(inputsOf(row)).toEqual([{ input: 1, role: "button", relay: 1 }]); }); }); describe("relayForButton / relayForPresence", () => { it("resolves a button + presence from inputs[]", () => { seed({ relays: [{ relay: 1, direction: "entry" }], inputs: [ { input: 1, role: "button", relay: 1 }, { input: 2, role: "presence", relay: 1, kind: "radar" }, ], }); const byBtn = relayForButton(db, CTL, 1); expect(byBtn).toMatchObject({ relay: 1, direction: "entry", presenceInput: 2, presenceKind: "radar" }); const byPres = relayForPresence(db, CTL, 2); expect(byPres).toMatchObject({ relay: 1, direction: "entry", presenceInput: 2 }); }); it("resolves IDENTICALLY from the legacy shape (no inputs[])", () => { seed({ relays: [{ relay: 1, direction: "entry", button: 1, presenceInput: 2, presenceKind: "loop" }] }); expect(relayForButton(db, CTL, 1)).toMatchObject({ relay: 1, presenceInput: 2, presenceKind: "loop" }); expect(relayForPresence(db, CTL, 2)).toMatchObject({ relay: 1, presenceInput: 2 }); }); it("resolves an EXIT presence row to the exit relay (the exit radar)", () => { seed({ relays: [ { relay: 1, direction: "entry" }, { relay: 2, direction: "exit" }, ], inputs: [ { input: 2, role: "presence", relay: 1, kind: "radar" }, // entry radar { input: 5, role: "presence", relay: 2, kind: "radar" }, // exit radar ], }); // NOTE: relayForPresence only gates entry/both relays (transient entry). The exit radar // resolves to null HERE (the exit barrier has no entry gate) — but it's still a valid // inputs[] row the lamp can trigger on. The entry radar resolves to relay 1. expect(relayForPresence(db, CTL, 2)).toMatchObject({ relay: 1 }); expect(relayForPresence(db, CTL, 5)).toBeNull(); // exit relay isn't a transient-entry gate }); it("a button on an exit-only relay is not a transient-entry trigger", () => { seed({ relays: [{ relay: 2, direction: "exit" }], inputs: [{ input: 1, role: "button", relay: 2 }], }); expect(relayForButton(db, CTL, 1)).toBeNull(); }); });