POSA_LEAKSMS/tests/leak_debounce_test.php
cw2007 2a469dc3cc feat(alert): 누수 감지/복구 디바운스 · 온습도 1시간 주기 · 센서별 수신자 그룹
누수 감지↔복구 플래핑 SMS 폭탄 방지, 온습도 측정 주기 1시간화(부수 오탐
차단), 센서별 SMS 수신자 그룹 지정 3종을 함께 반영한다.

1) 누수 디바운스 (서버 전용, 펌웨어 무변경)
   - leak_sensor_status 에 leak_confirmed / leak_pending_since 컬럼 추가.
   - leak_debounce_decide(): 단일 대칭 타이머 순수 함수. raw 상태가 확인창
     동안 지속돼야 확정하고 확정 엣지에서만 SMS 1회. dry 블립<복구창이면
     복구 미확정 → 확정 유지 → 재감지·복구 SMS 둘 다 차단(폭탄 원천 봉쇄).
   - apply_leak_debounce()/sweep_pending_leak_transitions(): 센서 네임드 락으로
     직렬화, cron sweep 으로 보고 끊겨도 확정 해소. 사고(leak_incident)
     open/close 도 확정 엣지로 이동(월간보고 사고건수 정합). raw 이벤트는
     leak_sensor_log 에 그대로 보존(플래핑 진단).
   - 컬럼 없는 구배포는 레거시 즉시 발송으로 graceful degrade.
   - 확인창 기본: 감지 60s / 복구 180s (config.local.php 조정).

2) 온습도 1시간 주기 + 센서별 히트비트 타임아웃
   - raspberry_pi4_sht30/config.py REPORT_INTERVAL 300→3600.
   - heartbeat_timeout_for(): 명시 오버라이드 > 온습도 기본(7500s) > 전역(1200s).
     cron_heartbeat / dashboard 오프라인 판정을 센서별로. 매시간 오탐 제거,
     누수 오프라인은 20분 유지.
   - STUCK_DETECT_SAMPLES 6→3(1시간 주기 ≈3시간), 관련 주석 정정.

3) 센서별 수신자 그룹
   - SENSOR_RECIPIENTS 맵 + recipients_for_sensor() 전역 폴백.
   - sms_dispatch_to_recipients 가 센서별로 수신자 해소(에스컬레이션 TIER2
     명시 전달은 유지, 헬스/잔액은 전역).

대시보드: 누수 카드 '확인중(디바운스)' 배지, 시스템 탭에 확인창·센서별
타임아웃·수신자 그룹 표시.

마이그레이션: sql/migration_leak_debounce.sql (컬럼 2개), schema_sht30.sql 갱신.
테스트: leak_debounce(12) / recipients_group / heartbeat_timeout 순수 단위 추가.
설계: docs/superpowers/specs/2026-07-13-leak-debounce-sensor-config-design.md
2026-07-13 20:57:17 +09:00

89 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
// 누수 감지/복구 디바운스 순수 판정 테스트 (DB/네트워크 없음).
// leak_debounce_decide(): raw 상태가 확인창(감지/복구) 동안 지속돼야 확정한다.
// 플래핑(감지↔복구 채터링)이 SMS 폭탄으로 번지지 않음을 회귀 방지.
$root = dirname(__DIR__);
require_once $root . '/php/sms_send.php'; // leak_debounce_decide() 포함
$failures = [];
function check(bool $cond, string $msg): void {
global $failures;
if (!$cond) { $failures[] = $msg; }
}
check(function_exists('leak_debounce_decide'), 'leak_debounce_decide() should exist.');
$ALERT = 60; // 감지 확인창
$RECOV = 180; // 복구 확인창
$NOW = 1_700_000_000;
// helper: 결과 비교
$eq = function (array $got, int $confirmed, ?int $pending, ?string $sms): bool {
return $got['confirmed'] === $confirmed
&& $got['pending_since'] === $pending
&& $got['sms'] === $sms;
};
// ── 1) 안정(정상): raw==confirmed==0, pending 없음 → 무변화 ──
check($eq(leak_debounce_decide(0, 0, null, $ALERT, $RECOV, $NOW), 0, null, null),
'stable normal -> no change, no sms');
// ── 2) 안정(누수): raw==confirmed==1, 잔여 pending → pending 해제 ──
check($eq(leak_debounce_decide(1, 1, $NOW - 40, $ALERT, $RECOV, $NOW), 1, null, null),
'stable leak with stale pending -> clears pending, no sms');
// ── 3) 누수 감지 시작: raw=1, confirmed=0, pending 없음 → 타이머 시작 ──
check($eq(leak_debounce_decide(1, 0, null, $ALERT, $RECOV, $NOW), 0, $NOW, null),
'leak onset -> start pending timer, no sms yet');
// ── 4) 감지 확인창 이내(경과 30 < 60) → 대기 유지 ──
check($eq(leak_debounce_decide(1, 0, $NOW - 30, $ALERT, $RECOV, $NOW), 0, $NOW - 30, null),
'leak within confirm window -> keep waiting, no sms');
// ── 5) 감지 확인(경과 60 >= 60) → 확정 + [누수감지] ──
check($eq(leak_debounce_decide(1, 0, $NOW - 60, $ALERT, $RECOV, $NOW), 1, null, 'leak'),
'leak sustained >= alert window -> confirm leak, sms=leak');
// ── 6) 감지 경계값: 경과 59 → 아직 대기(엄격 <) ──
check($eq(leak_debounce_decide(1, 0, $NOW - 59, $ALERT, $RECOV, $NOW), 0, $NOW - 59, null),
'leak window-1 -> still waiting');
// ── 7) 복구 시작: raw=0, confirmed=1, pending 없음 → 타이머 시작 ──
check($eq(leak_debounce_decide(0, 1, null, $ALERT, $RECOV, $NOW), 1, $NOW, null),
'recovery onset -> start pending timer, no sms yet');
// ── 8) 복구 확인창 이내(경과 100 < 180) → 대기 유지 ──
check($eq(leak_debounce_decide(0, 1, $NOW - 100, $ALERT, $RECOV, $NOW), 1, $NOW - 100, null),
'recovery within confirm window -> keep waiting, no sms');
// ── 9) 복구 확인(경과 180 >= 180) → 확정 + [누수복구] ──
check($eq(leak_debounce_decide(0, 1, $NOW - 180, $ALERT, $RECOV, $NOW), 0, null, 'recovery'),
'recovery sustained >= recovery window -> confirm normal, sms=recovery');
// ── 10) [핵심] 확정 누수 중 짧은 dry 블립: 확정상태로 raw 복귀 → pending 해제(복구 확정 안 됨) ──
// 시퀀스: 누수 확정(confirmed=1). dry 블립 와서 pending 시작(경과 50<180). 다시 leak 들어옴.
// leak 재수신: raw=1==confirmed=1 → pending 해제, sms 없음. => 복구/재감지 SMS 둘 다 안 나감.
check($eq(leak_debounce_decide(1, 1, $NOW - 50, $ALERT, $RECOV, $NOW), 1, null, null),
'leak resumes before recovery confirms -> clears pending, NO recovery/re-alert sms (bomb guard)');
// ── 11) [핵심] 정상 중 짧은 leak 블립: 확정상태로 raw 복귀 → pending 해제(감지 확정 안 됨) ──
check($eq(leak_debounce_decide(0, 0, $NOW - 20, $ALERT, $RECOV, $NOW), 0, null, null),
'dry resumes before alert confirms -> clears pending, NO alert sms (blip filtered)');
// ── 12) 비대칭 임계 검증: 복구는 alert 창(60)이 아니라 recovery 창(180) 사용 ──
// 경과 100: alert 창(60)이면 확정되겠지만 recovery 이므로 아직 대기여야 한다.
check($eq(leak_debounce_decide(0, 1, $NOW - 100, $ALERT, $RECOV, $NOW), 1, $NOW - 100, null),
'recovery uses recovery window (100 < 180) not alert window (60)');
if ($failures) {
fwrite(STDERR, "Leak debounce test failures:\n");
foreach ($failures as $f) {
fwrite(STDERR, "- {$f}\n");
}
exit(1);
}
echo "Leak debounce decision checks passed.\n";