Files
hackerspace.sh/__tests__/door-log-logic.test.ts
T
Moheeb Zara 0ac69abc8e door ingest: anchor card matching to the HeatSync hex-uid model
The inbound-ingest matcher read a stored card_uid as both decimal and hex,
which mis-attributed all-digit uids (a stored "16" is hex 0x16 = 22, but it
wrongly matched a decimal poll of card 16) and was conceptually wrong.
HeatSync stores the uid as hex (the grant encoder requires it) and the ?z
log / webhooks report the decimal card number, so cardMatchesEvent now
matches a reported decimal when hexInt(storedUid) equals it, and matches a
webhook card_uid by exact string. This removes the false positive while
keeping every legit match. Attribution is audit-only (never an access
decision) and inert until a space enables ingest; no migration.

Also records the pass-63/64 door-epic deploy state and the pass-65 audit in
the handoff log.
2026-05-20 21:31:11 -07:00

164 lines
6.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
parseHeatSyncLog,
normalizeWebhookEvents,
cardMatchesEvent,
type DoorIngestEvent,
} from '@/lib/door-log-logic'
function ev(p: Partial<DoorIngestEvent>): DoorIngestEvent {
return { result: 'granted', cardNumber: null, cardUid: null, occurredAt: null, detail: '', dedupeKey: '', ...p }
}
describe('parseHeatSyncLog', () => {
it('parses a granted event and reconstructs a small card number', () => {
const log = '<pre>\nG: 12345\ng: 0\n</pre>'
const ev = parseHeatSyncLog(log)
expect(ev).toHaveLength(1)
expect(ev[0].result).toBe('granted')
expect(ev[0].cardNumber).toBe('12345')
expect(ev[0].cardUid).toBeNull()
})
it('reconstructs a card number split across the divisor (high*32767 + low)', () => {
// 40000 = 1*32767 + 7233
const log = 'G: 7233\ng: 1'
const ev = parseHeatSyncLog(log)
expect(ev[0].cardNumber).toBe('40000')
})
it('parses a denied event from D/d', () => {
const log = 'D: 100\nd: 0'
const ev = parseHeatSyncLog(log)
expect(ev[0].result).toBe('denied')
expect(ev[0].cardNumber).toBe('100')
})
it('stamps best-effort H:M:E time-of-day when present', () => {
const log = 'H: 9\nM: 5\nE: 3\nG: 50\ng: 0'
const ev = parseHeatSyncLog(log)
expect(ev[0].detail).toContain('09:05:03')
expect(ev[0].dedupeKey).toContain('09:05:03')
})
it('ignores empty ring slots (NUL keys) and unrelated keys', () => {
const log = ': 0\nU: 1\nG: 7\ng: 0\n: 0'
const ev = parseHeatSyncLog(log)
expect(ev).toHaveLength(1)
expect(ev[0].cardNumber).toBe('7')
})
it('does not ingest raw reads (R/r)', () => {
const log = 'R: 999\nr: 0'
expect(parseHeatSyncLog(log)).toHaveLength(0)
})
it('gives distinct dedupe keys to events in different slots, stable per slot', () => {
const log = 'G: 1\ng: 0\nU: 1\nG: 1\ng: 0'
const ev = parseHeatSyncLog(log)
expect(ev).toHaveLength(2)
expect(ev[0].dedupeKey).not.toBe(ev[1].dedupeKey)
// Re-parsing the identical dump yields identical keys (re-poll dedupes).
const again = parseHeatSyncLog(log)
expect(again.map(e => e.dedupeKey)).toEqual(ev.map(e => e.dedupeKey))
})
it('caps the number of emitted events', () => {
const log = Array.from({ length: 50 }, () => 'G: 1\ng: 0').join('\n')
expect(parseHeatSyncLog(log, { maxEvents: 10 })).toHaveLength(10)
})
it('returns nothing for an empty or junk body', () => {
expect(parseHeatSyncLog('<pre>\n</pre>')).toHaveLength(0)
expect(parseHeatSyncLog('not a log at all')).toHaveLength(0)
})
it('ignores pathological over-long digit runs without throwing (hostile body)', () => {
// Number('9'.repeat(400)) === Infinity and BigInt(Infinity) throws; the
// 15-digit cap means this line never matches and is skipped.
const log = `G: ${'9'.repeat(400)}\ng: 0\nG: 42\ng: 0`
expect(() => parseHeatSyncLog(log)).not.toThrow()
const ev = parseHeatSyncLog(log)
expect(ev).toHaveLength(1)
expect(ev[0].cardNumber).toBe('42')
})
it('treats a high-half as 0 when missing within the window', () => {
const log = 'G: 7'
const ev = parseHeatSyncLog(log)
expect(ev[0].cardNumber).toBe('7')
})
it('does not pair a G with a distant unrelated g (window bound)', () => {
// A g far past the window must not be borrowed as this G's high half.
const log = 'G: 5\nU: 1\nU: 1\nU: 1\nU: 1\ng: 9'
const ev = parseHeatSyncLog(log)
expect(ev[0].cardNumber).toBe('5')
})
})
describe('normalizeWebhookEvents', () => {
it('keys on the caller-supplied event id', () => {
const ev = normalizeWebhookEvents([{ id: 'abc', card_uid: '9c40', result: 'granted' }])
expect(ev[0].dedupeKey).toBe('wh:abc')
expect(ev[0].cardUid).toBe('9c40')
expect(ev[0].result).toBe('granted')
})
it('defaults a missing result to unknown', () => {
const ev = normalizeWebhookEvents([{ id: '1', card_number: '40000' }])
expect(ev[0].result).toBe('unknown')
expect(ev[0].cardNumber).toBe('40000')
})
it('passes through an ISO occurred_at', () => {
const ev = normalizeWebhookEvents([{ id: '1', card_uid: 'x', occurred_at: '2026-05-20T10:00:00Z' }])
expect(ev[0].occurredAt).toBe('2026-05-20T10:00:00Z')
})
})
describe('cardMatchesEvent', () => {
it('matches a hex-stored uid to a decimal poll number', () => {
// 0x9c40 === 40000
expect(cardMatchesEvent('9c40', ev({ cardNumber: '40000' }))).toBe(true)
expect(cardMatchesEvent('9C40', ev({ cardNumber: '40000' }))).toBe(true)
expect(cardMatchesEvent('0x9c40', ev({ cardNumber: '40000' }))).toBe(true)
})
it('does NOT match a different card by coincidental decimal string (the fixed false positive)', () => {
// stored "16" is hex 0x16 = 22; a poll of decimal card 16 must NOT match it.
expect(cardMatchesEvent('16', ev({ cardNumber: '16' }))).toBe(false)
// stored "16" DOES match a poll of decimal 22 (its real number).
expect(cardMatchesEvent('16', ev({ cardNumber: '22' }))).toBe(true)
})
it('matches an all-digit hex uid to its decimal number', () => {
// stored "10" is hex 0x10 = 16; a poll of 16 is the same card.
expect(cardMatchesEvent('10', ev({ cardNumber: '16' }))).toBe(true)
})
it('matches a webhook card_uid by exact (case-insensitive) string', () => {
expect(cardMatchesEvent('9c40', ev({ cardUid: '9C40' }))).toBe(true)
})
it('matches a webhook all-digit card_uid as a decimal number (hex-stored)', () => {
expect(cardMatchesEvent('9c40', ev({ cardUid: '40000' }))).toBe(true)
})
it('matches a webhook card_number field', () => {
expect(cardMatchesEvent('9c40', ev({ cardUid: null, cardNumber: '40000' }))).toBe(true)
})
it('does not match different cards or empty input', () => {
expect(cardMatchesEvent('9c40', ev({ cardNumber: '40001' }))).toBe(false)
expect(cardMatchesEvent('', ev({ cardNumber: '40000' }))).toBe(false)
expect(cardMatchesEvent('9c40', ev({}))).toBe(false)
})
it('does not match a non-hex stored uid against a number', () => {
// 'xyz' is not a HeatSync hex uid; only an exact webhook uid string matches.
expect(cardMatchesEvent('xyz', ev({ cardNumber: '123' }))).toBe(false)
expect(cardMatchesEvent('xyz', ev({ cardUid: 'xyz' }))).toBe(true)
})
})