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>
199 lines
7.8 KiB
Python
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)
|