no breaches'); eq(evaluate_metric_thresholds($tMid, $hMid)['status'], 'normal', 'normal -> status normal'); eq(evaluate_metric_thresholds($TH + 5, $hMid)['breaches'], ['high_temp'], 'above high temp -> high_temp'); eq(evaluate_metric_thresholds($TL - 5, $hMid)['breaches'], ['low_temp'], 'below low temp -> low_temp'); eq(evaluate_metric_thresholds($tMid, $HH + 5)['breaches'], ['high_humidity'], 'above high rh -> high_humidity'); eq(evaluate_metric_thresholds($tMid, $HL - 5)['breaches'], ['low_humidity'], 'below low rh -> low_humidity'); // 경계값 포함(>= / <=) eq(evaluate_metric_thresholds($TH, $hMid)['breaches'], ['high_temp'], 'temp == high threshold -> high_temp (inclusive)'); eq(evaluate_metric_thresholds($TL, $hMid)['breaches'], ['low_temp'], 'temp == low threshold -> low_temp (inclusive)'); eq(evaluate_metric_thresholds($tMid, $HH)['breaches'], ['high_humidity'], 'rh == high threshold -> high_humidity (inclusive)'); eq(evaluate_metric_thresholds($tMid, $HL)['breaches'], ['low_humidity'], 'rh == low threshold -> low_humidity (inclusive)'); // 동시 위반 + 대표 상태(첫 breach) eq(evaluate_metric_thresholds($TH + 5, $HH + 5)['breaches'], ['high_temp', 'high_humidity'], 'high temp+humidity -> both breaches'); eq(evaluate_metric_thresholds($TH + 5, $HH + 5)['status'], 'high_temp', 'status = first breach'); // 결측(null) 처리 eq(evaluate_metric_thresholds(null, $HH + 5)['breaches'], ['high_humidity'], 'null temp + high rh -> high_humidity only'); eq(evaluate_metric_thresholds($TH + 5, null)['breaches'], ['high_temp'], 'high temp + null rh -> high_temp only'); eq(evaluate_metric_thresholds(null, null)['breaches'], [], 'all null -> no breaches'); // ── metric_is_recovered (히스테리시스) ────────────────────── check(metric_is_recovered($tMid, $hMid) === true, 'mid values are recovered'); check(metric_is_recovered($TH + 5, $hMid) === false, 'above high temp not recovered'); check(metric_is_recovered($tMid, $HH + 5) === false, 'above high rh not recovered'); // 임계 바로 안쪽(히스테리시스 데드밴드)은 복구로 보지 않음 check(metric_is_recovered($TH - ((float)METRIC_TEMP_HYSTERESIS_C / 2.0), $hMid) === false, 'temp inside hysteresis deadband not recovered'); check(metric_is_recovered($tMid, $HH - ((float)METRIC_RH_HYSTERESIS / 2.0)) === false, 'rh inside hysteresis deadband not recovered'); // 결측은 해당 지표 정상 간주 check(metric_is_recovered(null, $hMid) === true, 'null temp recovered when rh normal'); check(metric_is_recovered($tMid, null) === true, 'null rh recovered when temp normal'); // ── metric_value_str ──────────────────────────────────────── eq(metric_value_str(30.0), '30', '30.0 -> 30'); eq(metric_value_str(25.5), '25.5', '25.5 -> 25.5'); eq(metric_value_str(-3.0), '-3', '-3.0 -> -3'); eq(metric_value_str(100.0), '100', '100.0 -> 100'); eq(metric_value_str(0.0), '0', '0.0 -> 0'); if ($failures) { fwrite(STDERR, "Threshold SMS logic test failures:\n"); foreach ($failures as $f) { fwrite(STDERR, "- {$f}\n"); } exit(1); } echo "Threshold SMS logic checks passed.\n";