누수/온습도 경보 쿨다운이 경보 태그만 조회해, 직전에 [누수복구]/[정상복귀]가 있었어도 이전 경보 발송 시각만 보고 억제했다. 그 결과 복구 직후 재발한 새 사고 (별개 에피소드)가 쿨다운 창(기본 30분) 동안 통째로 누락됐다. - alert_cooldown_suppresses(): 경보/복구 중 "최신"이 경보이고(=미복구) 쿨다운 이내일 때만 억제하는 순수 헬퍼 추가. 최신이 복구면 새 에피소드로 즉시 발송. - send_leak_alert_sms / send_metric_alert_sms: 쿨다운 조회를 (경보|복구) 최신 1건 기준으로 교체하고 위 헬퍼로 판정. 지속 경보 폭주 방지는 그대로 유지. - tests/cooldown_episode_test.php: 복구 직후 재발 발송 등 9종 회귀 추가, run_tests.sh 에 등록. - tests/security_hardening_test.php: dew_point_c 의 forecast.php 단일출처 이동에 맞춰 낡은 단언 갱신(정의는 forecast.php, sms_send.php 는 사용 확인). 작업 트리에 있던 dew_point_c 중복정의 제거(forecast.php 단일출처화)도 sms_send.php 변경에 함께 포함된다.
76 lines
4 KiB
PHP
76 lines
4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// 에피소드 인지 쿨다운 회귀 테스트 (DB/네트워크 없이 순수 로직만).
|
|
// alert_cooldown_suppresses(): 복구 직후 재발한 경보를 쿨다운이 삼키던 버그 회귀 방지.
|
|
//
|
|
// 배경(버그): send_leak_alert_sms / send_metric_alert_sms 의 쿨다운이 경보 태그만 보고
|
|
// "최근 N초 내 경보 있으면 생략" 했다. 그 사이 [누수복구]/[정상복귀] 가 있었어도 무시 →
|
|
// 복구 직후 재발한 새 사고(별개 에피소드)가 통째로 누락됐다.
|
|
// 수정: 경보/복구 중 "최신"이 경보이고(=미복구) 쿨다운 이내일 때만 생략. 최신이 복구면 발송.
|
|
|
|
$root = dirname(__DIR__);
|
|
require_once $root . '/php/sms_send.php'; // config.php(태그 상수) + alert_cooldown_suppresses() 포함
|
|
|
|
$failures = [];
|
|
function check(bool $cond, string $msg): void {
|
|
global $failures;
|
|
if (!$cond) { $failures[] = $msg; }
|
|
}
|
|
|
|
check(function_exists('alert_cooldown_suppresses'), 'alert_cooldown_suppresses() should exist.');
|
|
|
|
$COOL = 1800; // 30분 쿨다운(테스트 고정값 — 상수와 무관하게 로직만 검증)
|
|
$now = 1_700_000_000; // 고정 기준시각(결정적). sent_at 은 date() 로 생성.
|
|
$at = fn(int $secAgo): string => date('Y-m-d H:i:s', $now - $secAgo);
|
|
|
|
$LEAK = SMS_TAG_LEAK_ALERT;
|
|
$LEAK_R = SMS_TAG_LEAK_RECOVERY;
|
|
$HITEMP = SMS_TAG_HIGH_TEMP;
|
|
$METRIC_R = SMS_TAG_RECOVERY;
|
|
$body = fn(string $tag): string => $tag . "\n위치: 시험\n센서: t";
|
|
|
|
// ── 1) 이력 없음 → 발송(생략 아님) ──────────────────────────
|
|
check(alert_cooldown_suppresses(null, null, $LEAK, $COOL, $now) === false,
|
|
'no history -> send (not suppressed)');
|
|
|
|
// ── 2) 지속 누수: 최신이 [누수감지] & 쿨다운 이내 → 생략 ─────
|
|
check(alert_cooldown_suppresses($body($LEAK), $at(60), $LEAK, $COOL, $now) === true,
|
|
'ongoing leak within cooldown -> suppress (flood guard kept)');
|
|
|
|
// ── 3) 오래된 누수: 최신이 [누수감지] & 쿨다운 경과 → 발송 ───
|
|
check(alert_cooldown_suppresses($body($LEAK), $at(3600), $LEAK, $COOL, $now) === false,
|
|
'stale leak beyond cooldown -> send (periodic re-alert)');
|
|
|
|
// ── 4) [핵심 버그] 복구 직후 재발: 최신이 [누수복구] → 발송 ──
|
|
// 복구가 방금(1초 전) 있었어도, 새 누수는 별개 에피소드이므로 쿨다운 무시하고 발송해야 한다.
|
|
check(alert_cooldown_suppresses($body($LEAK_R), $at(1), $LEAK, $COOL, $now) === false,
|
|
'BUG REGRESSION: leak right after [누수복구] -> send (must NOT be suppressed)');
|
|
|
|
// ── 5) 경계값: elapsed == cooldown 은 생략 아님(< 비교) ──────
|
|
check(alert_cooldown_suppresses($body($LEAK), $at($COOL), $LEAK, $COOL, $now) === false,
|
|
'elapsed exactly == cooldown -> send (strict <)');
|
|
check(alert_cooldown_suppresses($body($LEAK), $at($COOL - 1), $LEAK, $COOL, $now) === true,
|
|
'elapsed cooldown-1 -> suppress');
|
|
|
|
// ── 6) 온습도 경보도 동일: 최신이 [정상복귀] → 발송 ─────────
|
|
check(alert_cooldown_suppresses($body($METRIC_R), $at(1), $HITEMP, $COOL, $now) === false,
|
|
'metric: high_temp right after [정상복귀] -> send (episode reset)');
|
|
// 지속 고온: 최신이 [고온경보] & 이내 → 생략
|
|
check(alert_cooldown_suppresses($body($HITEMP), $at(60), $HITEMP, $COOL, $now) === true,
|
|
'metric: ongoing high_temp within cooldown -> suppress');
|
|
|
|
// ── 7) 다른 종류 경보가 최신이면 이 종류 쿨다운에 안 걸림 ────
|
|
// (쿼리도 이 종류+복구만 뽑지만, 순수함수는 prefix 로 방어)
|
|
check(alert_cooldown_suppresses($body(SMS_TAG_HIGH_HUMIDITY), $at(60), $HITEMP, $COOL, $now) === false,
|
|
'metric: latest is different-kind alert -> this kind not suppressed');
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "Cooldown episode test failures:\n");
|
|
foreach ($failures as $f) {
|
|
fwrite(STDERR, "- {$f}\n");
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
echo "Cooldown episode-aware checks passed.\n";
|