zioinfo-mail/itsm/test_c345.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

199 lines
7.8 KiB
Python

"""C-3 Problem Management / C-4 용량 관리 / C-5 서비스 카탈로그 통합 테스트"""
import sys, ast, os
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-c345-secret-key-32bytes-padded!")
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_c345.db")
ok = True
print("=== 1. 구문 검사 ===")
files = [
"routers/problem.py", "routers/capacity.py", "routers/catalog.py",
"models.py", "main.py"
]
for f in files:
try:
with open(f, encoding="utf-8") as fh:
src = fh.read()
ast.parse(src)
print(f" OK {f}")
except SyntaxError as e:
print(f" ERR {f}: {e}")
ok = False
print("\n=== 2. C-3 Problem Management 확인 ===")
with open("models.py", encoding="utf-8") as f:
models_src = f.read()
with open("routers/problem.py", encoding="utf-8") as f:
prob_src = f.read()
prob_checks = [
(models_src, "class ProblemRecord(Base):", "ProblemRecord ORM"),
(models_src, "class ProblemNote(Base):", "ProblemNote ORM"),
(models_src, "class ProblemStatus(str, Enum):", "ProblemStatus Enum"),
(models_src, "INVESTIGATING", "INVESTIGATING 상태"),
(models_src, "RCA_DONE", "RCA_DONE 상태"),
(models_src, "WORKAROUND", "WORKAROUND 상태"),
(models_src, "known_error", "known_error 컬럼"),
(models_src, "root_cause", "root_cause 컬럼"),
(models_src, "tb_problem", "tb_problem 테이블"),
(prob_src, '@router.post("/"', "POST 문제 생성"),
(prob_src, '@router.get("/known-errors"', "Known Error DB"),
(prob_src, '@router.post("/{prb_id}/rca"', "RCA 기록"),
(prob_src, '@router.post("/{prb_id}/workaround"', "임시 해결"),
(prob_src, '@router.post("/{prb_id}/resolve"', "해결 처리"),
(prob_src, '@router.post("/{prb_id}/close"', "종결 처리"),
(prob_src, '@router.post("/{prb_id}/notes"', "활동 노트"),
(prob_src, "PRB-", "Problem ID 형식 (PRB-)"),
]
for src, sym, desc in prob_checks:
status = "OK" if sym in src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {desc}")
print("\n=== 3. C-4 용량 관리 확인 ===")
with open("routers/capacity.py", encoding="utf-8") as f:
cap_src = f.read()
cap_checks = [
(models_src, "class CapacityPlan(Base):", "CapacityPlan ORM"),
(models_src, "class CapacityStatus(str, Enum):", "CapacityStatus Enum"),
(models_src, "forecast_3m", "3개월 예측 컬럼"),
(models_src, "forecast_6m", "6개월 예측 컬럼"),
(models_src, "forecast_12m", "12개월 예측 컬럼"),
(models_src, "expansion_needed_at", "확장 필요 시점 컬럼"),
(models_src, "growth_rate", "월 성장률 컬럼"),
(models_src, "tb_capacity_plan", "tb_capacity_plan 테이블"),
(cap_src, '@router.get("/dashboard"', "대시보드"),
(cap_src, '@router.post("/plans"', "용량 계획 등록"),
(cap_src, '@router.get("/plans"', "용량 계획 목록"),
(cap_src, '@router.post("/plans/{plan_id}/recalculate"', "재계산"),
(cap_src, '@router.get("/alerts"', "경보 목록"),
(cap_src, '@router.get("/trends/{source}"', "트렌드"),
(cap_src, "_calc_forecasts", "예측 계산 함수"),
(cap_src, "_calc_status", "상태 계산 함수"),
(cap_src, "OVERLOAD", "OVERLOAD 상태"),
]
for src, sym, desc in cap_checks:
status = "OK" if sym in src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {desc}")
print("\n=== 4. C-5 서비스 카탈로그 확인 ===")
with open("routers/catalog.py", encoding="utf-8") as f:
cat_src = f.read()
cat_checks = [
(models_src, "class ServiceItem(Base):", "ServiceItem ORM"),
(models_src, "class ServiceStatus(str, Enum):", "ServiceStatus Enum"),
(models_src, "sla_response_h", "응답 SLA 컬럼"),
(models_src, "sla_resolve_h", "해결 SLA 컬럼"),
(models_src, "sla_availability", "가용성 SLA 컬럼"),
(models_src, "approval_required", "승인 필요 컬럼"),
(models_src, "request_count", "요청 카운트 컬럼"),
(models_src, "tb_service_catalog", "tb_service_catalog 테이블"),
(cat_src, '@router.get("/"', "카탈로그 목록"),
(cat_src, '@router.post("/"', "카탈로그 등록"),
(cat_src, '@router.get("/{service_id}"', "서비스 상세"),
(cat_src, '@router.post("/{service_id}/request"', "서비스 요청 (SR 생성)"),
(cat_src, '@router.get("/categories"', "카테고리 목록"),
(cat_src, '@router.get("/stats"', "통계"),
(cat_src, "SVC-", "서비스 ID 형식 (SVC-)"),
(cat_src, "PENDING_APPROVAL", "승인 필요 SR 상태"),
]
for src, sym, desc in cat_checks:
status = "OK" if sym in src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {desc}")
print("\n=== 5. main.py 등록 확인 ===")
with open("main.py", encoding="utf-8") as f:
main_src = f.read()
for sym, desc in [
("problem", "C-3 problem 라우터"),
("problem.router", "problem 라우터 등록"),
("capacity", "C-4 capacity 라우터"),
("capacity.router", "capacity 라우터 등록"),
("catalog", "C-5 catalog 라우터"),
("catalog.router", "catalog 라우터 등록"),
]:
status = "OK" if sym in main_src else "ERR"
if status == "ERR":
ok = False
print(f" {status} {desc}")
print("\n=== 6. _calc_forecasts 수학 검증 ===")
try:
import importlib.util, math
spec = importlib.util.spec_from_file_location("cap_mod", "routers/capacity.py")
cap_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cap_mod)
# 월 10% 성장률로 현재값 100
f3, f6, f12, _ = cap_mod._calc_forecasts(100.0, 10.0, 200.0)
expected_f3 = round(100.0 * 1.1**3, 2) # 133.1
expected_f12 = round(100.0 * 1.1**12, 2) # 313.84
assert abs(f3 - expected_f3) < 0.1, f"3개월 예측 오류: {f3} != {expected_f3}"
assert abs(f12 - expected_f12) < 0.5, f"12개월 예측 오류: {f12} != {expected_f12}"
print(f" OK forecast(100, 10%, 3M) = {f3} (기대: {expected_f3})")
print(f" OK forecast(100, 10%, 12M) = {f12} (기대: {expected_f12})")
# 성장률 0이면 None 반환
f3_0, _, _, _ = cap_mod._calc_forecasts(100.0, 0.0, 200.0)
assert f3_0 is None, f"성장률 0에서 None 반환해야 함: {f3_0}"
print(f" OK 성장률 0 → None 반환")
except AssertionError as e:
print(f" ERR {e}")
ok = False
except Exception as e:
print(f" ERR _calc_forecasts 오류: {type(e).__name__}: {e}")
ok = False
print("\n=== 7. _calc_status 임계값 검증 ===")
try:
# OVERLOAD: current >= crit * 1.1
status = cap_mod._calc_status(100.0, 75.0, 90.0)
assert status == "OVERLOAD", f"OVERLOAD 판정 실패: {status}"
print(f" OK 100% (crit=90%) → {status}")
status = cap_mod._calc_status(92.0, 75.0, 90.0)
assert status == "CRITICAL", f"CRITICAL 판정 실패: {status}"
print(f" OK 92% (crit=90%) → {status}")
status = cap_mod._calc_status(80.0, 75.0, 90.0)
assert status == "WARNING", f"WARNING 판정 실패: {status}"
print(f" OK 80% (warn=75%) → {status}")
status = cap_mod._calc_status(50.0, 75.0, 90.0)
assert status == "NORMAL", f"NORMAL 판정 실패: {status}"
print(f" OK 50% → {status}")
except AssertionError as e:
print(f" ERR {e}")
ok = False
except Exception as e:
print(f" ERR _calc_status 오류: {type(e).__name__}: {e}")
print("\n=== 8. Problem / Capacity / Service ID 형식 검증 ===")
from datetime import datetime
today = datetime.utcnow().strftime("%Y%m%d")
ids = {
f"PRB-{today}-0001": 17, # PRB-YYYYMMDD-NNNN = 4+8+1+4 = 17
f"SVC-0001": 8, # SVC-NNNN = 4+4 = 8
}
for id_val, expected_len in ids.items():
status = "OK" if len(id_val) == expected_len else "WARN"
print(f" {status} {id_val} ({len(id_val)}자, 기대:{expected_len}자)")
print("\n=== C-3/C-4/C-5 통합 테스트 완료 ===")
if ok:
print("모든 검사 통과")
else:
sys.exit(1)