"""모델 대기 감지기 (보호구 · 근접 · 화재). 모델이 없는 상태가 **정상**이라는 것이 이 테스트들의 요지다. 배선은 있고 계약은 고정되어 있으며, 모델이 확보되는 날 바꿀 것은 Model 구현 하나뿐이어야 한다. """ from edge.detectors.pending import PendingConfig, PendingDetectors class _Model: def __init__(self, out=None, raises=False): self.out = out or [] self.raises = raises self.calls = 0 def detect(self, frame, persons): self.calls += 1 if self.raises: raise RuntimeError("hef not loaded") return self.out def test_disabled_by_default_and_never_calls_a_model(): m = _Model([{"kind": "ppe_missing", "confidence": 1.0}]) d = PendingDetectors(model=m) assert d.active is False assert d.observe(None, [], 0.0) == [] assert m.calls == 0, "비활성이면 추론 비용도 들지 않아야 한다" def test_enabled_without_a_model_is_still_inert(): # 가장 위험한 상태를 막는다: "곧 됩니다"로 켜 두면 현장은 감지되고 있다고 믿는다. d = PendingDetectors(PendingConfig(enabled=True), model=None) assert d.active is False assert d.observe(None, [], 0.0) == [] def test_emits_when_a_model_is_supplied(): m = _Model([{"kind": "ppe_missing", "confidence": 0.9, "track_id": 3, "spatial": (0.4, 0.7), "metrics": {"missing": "helmet"}}]) d = PendingDetectors(PendingConfig(enabled=True), model=m) assert d.active is True out = d.observe(None, [], 0.0) assert len(out) == 1 kind, metrics, track_id, spatial = out[0] assert kind == "ppe_missing" assert track_id == 3 assert spatial == (0.4, 0.7) assert metrics["missing"] == "helmet" assert metrics["confidence"] == 0.9 def test_low_confidence_is_dropped(): # 보호구 오탐은 곧 현장 반발이고, 반발은 시스템 전원을 내리는 것으로 끝난다. m = _Model([{"kind": "ppe_missing", "confidence": 0.5}]) d = PendingDetectors(PendingConfig(enabled=True), model=m) assert d.observe(None, [], 0.0) == [] def test_unknown_kind_is_ignored(): m = _Model([{"kind": "telepathy", "confidence": 1.0}]) d = PendingDetectors(PendingConfig(enabled=True), model=m) assert d.observe(None, [], 0.0) == [] def test_reemit_window_collapses_a_sustained_condition_into_one_event(): m = _Model([{"kind": "ppe_missing", "confidence": 0.99, "track_id": 1}]) cfg = PendingConfig(enabled=True) cfg.reemit_s["ppe_missing"] = 600.0 d = PendingDetectors(cfg, model=m) assert len(d.observe(None, [], 0.0)) == 1 assert d.observe(None, [], 60.0) == [], "한 사람이 한 시간을 그러고 있어도 사건은 하나다" assert len(d.observe(None, [], 700.0)) == 1 def test_model_failure_does_not_break_the_pipeline(): logged = [] d = PendingDetectors(PendingConfig(enabled=True), model=_Model(raises=True), log=logged.append) assert d.observe(None, [], 0.0) == [] assert logged, "실패는 조용히 넘어가지 않는다" def test_forget_clears_per_track_state(): m = _Model([{"kind": "proximity", "confidence": 0.99, "track_id": 5}]) d = PendingDetectors(PendingConfig(enabled=True), model=m) d.observe(None, [], 0.0) d.forget(5) assert len(d.observe(None, [], 1.0)) == 1, "트랙이 사라지면 억제 상태도 사라진다" def test_garbage_model_output_is_survived(): m = _Model(["nope", None, 42, {"no_kind": 1}]) d = PendingDetectors(PendingConfig(enabled=True), model=m) assert d.observe(None, [], 0.0) == []