4개 관점 병렬 감사 후 각 발견을 반증 시도해 확정. 반증 5건은 문서에 남겼다. 이번 감사의 요지: **구현도 단위테스트도 초록인데 납품 경로에서 동작하지 않는** 결함이 셋 있었다. 전부 같은 모양이다 — 테스트가 기본값이 아닌 설정을 썼거나, 값이 존재해도 되는지 결정하는 상류 허용 목록을 보지 않았다. CRITICAL 4건: R4-1 추락 승격이 기본 설정에서 한 번도 발화하지 않았다. link_s(3.0)를 now 기준으로 쟀는데 넘어짐은 prone_confirm_s(3.0) 지속 후 확정되므로 확정 시점에는 항상 3초 넘게 지나 있다. 다른 테스트가 전부 prone_confirm_s=0.5 를 써서 초록이었다. 착지(prone 시작) 시각과 비교하도록 고치고, 기본값 그대로 도는 회귀 테스트를 넣었다. R4-2 max_workers:0 이 상류 두 곳에서 막혀 있었다 — 서버 규칙 경계가 [1,50] 이라 저장이 400 이고, 엣지 파서가 v<=0 으로 규칙 전체를 무효화해 같이 넣은 2인1조까지 사라졌다. 앞 커밋에서 고친 네 곳은 전부 이 둘보다 하류라 도달 불가였다. R4-3 고소작업 프리셋(elevated)이 완전히 무동작이었다. 서버 경계에 키가 없어 400, 통과해도 엣지 파서가 침묵 삭제. 프리셋 안내문과 커밋 메시지가 시스템이 하지 않는 일을 약속하고 있었다. R4-4 동시 다발이 한 사람을 두 명으로 셀 수 있었다. 트래커는 1초 가려짐에 트랙을 버리고 새 id 를 주고(max_age=1.0) 누운 사람은 새 트랙에서도 즉시 prone 이라 두 번째 fall 이 난다. 그러면 다친 한 사람에게 "공기호흡기 없이 진입 금지"가 나가 구조가 늦어진다 — 기능의 가치가 정반대로 뒤집힌다. ghost 를 인원 집계에서 빼고, 거의 같은 자리의 사건은 같은 사람으로 본다. HIGH 5건: 구역 그룹핑 사문화(엣지가 zone_id 를 안 실어 카메라가 다른 두 희생자가 절대 안 묶였다 — 밀폐공간이 정확히 그 상황), 창 미끄러짐으로 한 사고가 매 분 새 위급 건으로 쏟아지고 인원이 줄면 앞선 알림을 스스로 뒤집던 문제, 무장 미만료·forget 누락·이중 발보, 업그레이드 안내의 마이그레이션 2개 누락(신규 유형 INSERT 가 전부 DB CHECK 에 걸린다), BUILD-INFO 커밋 스탬프. BUILD-INFO 는 1.1.0 에서 고친 R3-6 의 재발이다. 절차로 막으려다 두 번 실패했으므로 HEAD 일치를 검사하는 테스트로 막았다. README 마이그레이션 목록도 같은 이유로 테스트를 붙였다. MEDIUM 14건 중 눈에 띄는 것: 정비 억제가 SCBA 경고를 삼켰고(허가가 발급된 그 구역·그 시간이 질식이 일어나는 자리다) 위급 3종을 억제 예외로 뺐다. 데모 시더 심각도가 제품과 7종 어긋나 fall 이 위급으로 찍혀 1.2.0 의 요점인 넘어짐/추락 구분이 데모에서 지워져 있었다 — 라벨과 달리 화면이 안 깨져 눈으로는 못 잡으므로 엣지를 단일 출처로 강제하는 테스트를 붙였다. 문서의 "17종"은 실제 20종(엣지 16 + 서버 4)으로 정정하고 신규 2종을 소개서 감지 표에 넣었다. 검증: pytest 335 passed / phpunit 269 passed. 신규 테스트 30건. dist 는 이 커밋 이후 재생성한다(BUILD-INFO 가 HEAD 를 가리켜야 한다). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBjuR7DW6PNNMows4KnbYU
56 lines
2.3 KiB
Python
56 lines
2.3 KiB
Python
"""데모 시더의 심각도가 실제 제품과 같은가.
|
|
|
|
시더는 데모·스크린샷·UI 검토에 쓰는 데이터를 만든다. 여기 심각도가 엣지와 다르면 검토자는
|
|
제품이 실제로 내지 않는 등급을 보고 판단하게 된다. 실제로 7종이 어긋나 있었고, 그중 `fall`
|
|
이 위급으로 찍혀 1.2.0 의 요점(넘어짐 HIGH / 추락 CRITICAL)이 데모에서 지워져 있었다.
|
|
|
|
라벨과 달리 심각도는 어긋나도 화면이 깨지지 않는다 — 그래서 사람 눈으로는 잡히지 않고
|
|
이 테스트가 필요하다.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from edge.events import EventType, Severity, _SEVERITY
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SEEDER = ROOT / "server" / "tools" / "seed_demo.php"
|
|
|
|
# 서버만 발행하는 유형 — 엣지 enum 에 없다.
|
|
SERVER_ONLY = {"unacked_escalation": "critical", "multi_down": "critical"}
|
|
|
|
|
|
def _seeder_map() -> dict[str, str]:
|
|
src = SEEDER.read_text(encoding="utf-8")
|
|
m = re.search(r"\$severityOf\s*=\s*\[(.*?)\];", src, re.S)
|
|
assert m, "seed_demo.php 의 $severityOf 배열을 찾지 못했습니다 (리팩터되었나요?)"
|
|
return dict(re.findall(r"'([a-z_]+)'\s*=>\s*'([a-z]+)'", m.group(1)))
|
|
|
|
|
|
def _expected() -> dict[str, str]:
|
|
out = {e.value: _SEVERITY.get(e, Severity.INFO).value for e in EventType}
|
|
out.update(SERVER_ONLY)
|
|
return out
|
|
|
|
|
|
def test_seeder_severity_matches_the_edge():
|
|
seeder, expected = _seeder_map(), _expected()
|
|
wrong = {k: (v, expected[k]) for k, v in seeder.items()
|
|
if k in expected and v != expected[k]}
|
|
assert not wrong, (
|
|
"데모 시더가 제품과 다른 심각도를 씁니다 (유형: 시더 -> 제품): "
|
|
+ ", ".join(f"{k}: {a} -> {b}" for k, (a, b) in sorted(wrong.items())))
|
|
|
|
|
|
def test_seeder_covers_every_type():
|
|
seeder, expected = _seeder_map(), _expected()
|
|
missing = set(expected) - set(seeder)
|
|
assert not missing, (
|
|
f"시더에 심각도가 없는 유형: {sorted(missing)} — "
|
|
"빠지면 기본값으로 조용히 잘못된 등급이 찍힙니다")
|
|
|
|
|
|
def test_seeder_has_no_unknown_types():
|
|
seeder, expected = _seeder_map(), _expected()
|
|
extra = set(seeder) - set(expected)
|
|
assert not extra, f"시더에만 있는 유형(오타이거나 삭제된 유형): {sorted(extra)}"
|