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>
200 lines
7.4 KiB
Python
200 lines
7.4 KiB
Python
"""B-4 KB 자동 업데이트 에이전트 테스트"""
|
|
import sys, ast, asyncio, os
|
|
|
|
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-b4-secret-key-32bytes-padded!")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_b4.db")
|
|
|
|
ok = True
|
|
|
|
print("=== 1. 구문 검사 ===")
|
|
files = ["core/kb_agent.py", "routers/kb_agent.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. models.py KBDocument 확장 컬럼 확인 ===")
|
|
with open("models.py", encoding="utf-8") as f:
|
|
models_src = f.read()
|
|
|
|
checks = [
|
|
("source_sr_id", "source_sr_id 컬럼 (KB-SR 연결)"),
|
|
("author", "author 컬럼 (kb-agent)"),
|
|
("updated_at", "updated_at 컬럼"),
|
|
]
|
|
for sym, desc in checks:
|
|
# models.py의 KBDocument 섹션 안에 있는지 확인
|
|
kb_start = models_src.find("class KBDocument(Base):")
|
|
kb_end = models_src.find("\n\nclass ", kb_start + 1)
|
|
kb_section = models_src[kb_start:kb_end] if kb_end > 0 else models_src[kb_start:]
|
|
status = "OK" if sym in kb_section else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 3. core/kb_agent.py 함수 확인 ===")
|
|
with open("core/kb_agent.py", encoding="utf-8") as f:
|
|
kb_src = f.read()
|
|
|
|
fn_checks = [
|
|
("def classify_category(", "카테고리 분류 함수"),
|
|
("def extract_tags_rule(", "태그 추출 함수"),
|
|
("async def extract_kb_with_llm(", "LLM KB 추출"),
|
|
("def extract_kb_rule(", "규칙 기반 KB 추출"),
|
|
("def compute_similarity(", "유사도 계산"),
|
|
("async def find_similar_kb(", "유사 KB 검색"),
|
|
("async def auto_create_kb_from_sr(", "SR→KB 자동 생성"),
|
|
("async def run_kb_agent_batch(", "일괄 처리"),
|
|
("doc_id_val", "doc_id 생성 로직"),
|
|
("_CATEGORY_KEYWORDS", "카테고리 키워드 맵"),
|
|
("OLLAMA_URL", "Ollama URL"),
|
|
]
|
|
for sym, desc in fn_checks:
|
|
status = "OK" if sym in kb_src else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 4. routers/kb_agent.py 엔드포인트 확인 ===")
|
|
with open("routers/kb_agent.py", encoding="utf-8") as f:
|
|
router_src = f.read()
|
|
|
|
endpoint_checks = [
|
|
('@router.post("/run")', "POST /api/kb-agent/run"),
|
|
('@router.post("/analyze/{sr_id}")', "POST /api/kb-agent/analyze/{sr_id}"),
|
|
('@router.get("/candidates")', "GET /api/kb-agent/candidates"),
|
|
('@router.get("/stats")', "GET /api/kb-agent/stats"),
|
|
('@router.post("/kb"', "POST /api/kb-agent/kb (수동 생성)"),
|
|
('@router.put("/kb/{kb_id}"', "PUT /api/kb-agent/kb/{id}"),
|
|
("run_kb_agent_batch", "배치 실행 함수 호출"),
|
|
("auto_create_kb_from_sr", "SR 분석 함수 호출"),
|
|
]
|
|
for sym, desc in endpoint_checks:
|
|
status = "OK" if sym in router_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()
|
|
|
|
main_checks = [
|
|
("kb_agent", "kb_agent 라우터 임포트"),
|
|
("kb_agent.router", "kb_agent 라우터 등록"),
|
|
]
|
|
for sym, desc in main_checks:
|
|
status = "OK" if sym in main_src else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 6. classify_category 테스트 ===")
|
|
try:
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("kb_mod", "core/kb_agent.py")
|
|
kb_mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(kb_mod)
|
|
|
|
cases = [
|
|
("서버 CPU 과부하", "서버 CPU 메모리 문제입니다", "서버 운영"),
|
|
("톰캣 배포 오류", "tomcat WAS 배포 실패", "배포"),
|
|
("오라클 DB 연결 실패", "database connection pool 소진", "DB"),
|
|
("SSL 인증서 만료", "HTTPS 보안 SSL 오류", "네트워크"),
|
|
]
|
|
for title, desc, expected in cases:
|
|
cat = kb_mod.classify_category(title, desc)
|
|
status = "OK" if cat == expected else f"WARN(got {cat}, expected {expected})"
|
|
print(f" {status} '{title}' → {cat}")
|
|
|
|
except Exception as e:
|
|
print(f" ERR classify_category 오류: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 7. extract_tags_rule 테스트 ===")
|
|
try:
|
|
cases2 = [
|
|
("톰캣 재기동", "tomcat restart java WAS", "", ["java", "restart", "tomcat"]),
|
|
("MySQL DB 연결", "mysql database connection", "", ["mysql"]),
|
|
("Nginx SSL 설정", "nginx ssl tls 설정", "", ["nginx", "ssl"]),
|
|
]
|
|
for title, desc, solution, expected_tags in cases2:
|
|
tags = kb_mod.extract_tags_rule(title, desc, solution)
|
|
# 기대 태그 중 하나라도 있으면 OK
|
|
found = [t for t in expected_tags if t in tags]
|
|
status = "OK" if found else f"WARN(got {tags}, expected subset of {expected_tags})"
|
|
print(f" {status} '{title}' → tags={tags}")
|
|
|
|
except Exception as e:
|
|
print(f" ERR extract_tags_rule 오류: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 8. compute_similarity 테스트 ===")
|
|
try:
|
|
cases3 = [
|
|
("서버 CPU 과부하 장애", "서버 CPU 과부하 장애", 1.0), # 동일
|
|
("서버 CPU 장애", "서버 메모리 문제", 0.1), # 낮은 유사도 (일부 겹침)
|
|
("완전 다른 텍스트 xyz", "전혀 관련 없음 abc def", 0.0), # 매우 낮음
|
|
]
|
|
for t1, t2, min_expected in cases3:
|
|
sim = kb_mod.compute_similarity(t1, t2)
|
|
status = "OK" if sim >= min_expected else f"WARN(got {sim:.2f}, min={min_expected})"
|
|
print(f" {status} 유사도({t1[:15]}|{t2[:15]}) = {sim:.2f}")
|
|
|
|
except Exception as e:
|
|
print(f" ERR compute_similarity 오류: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 9. extract_kb_rule 테스트 ===")
|
|
try:
|
|
result = kb_mod.extract_kb_rule(
|
|
title="서버 CPU 과부하 장애",
|
|
description="CPU 사용률이 95%를 초과하여 서비스가 중단됨",
|
|
work_log="$ top -bn1 | head -20\n$ kill -9 12345",
|
|
sr_type="OTHER",
|
|
)
|
|
assert "title" in result, "title 없음"
|
|
assert "category" in result, "category 없음"
|
|
assert "symptom" in result, "symptom 없음"
|
|
assert "solution" in result, "solution 없음"
|
|
assert "tags" in result, "tags 없음"
|
|
assert len(result["commands"]) > 0, f"commands 없음: {result['commands']}"
|
|
print(f" OK KB 추출: title='{result['title'][:40]}', category={result['category']}")
|
|
print(f" OK commands 추출: {result['commands']}")
|
|
|
|
except AssertionError as e:
|
|
print(f" ERR extract_kb_rule 오류: {e}")
|
|
ok = False
|
|
except Exception as e:
|
|
print(f" ERR extract_kb_rule 예외: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 10. Ollama 폴백 테스트 ===")
|
|
async def test_llm_fallback():
|
|
try:
|
|
result = await kb_mod.extract_kb_with_llm(
|
|
title="서버 오류",
|
|
description="CPU 과부하",
|
|
work_log="재기동 완료",
|
|
sr_type="OTHER",
|
|
model="llama3",
|
|
timeout=2,
|
|
)
|
|
assert result is None or isinstance(result, dict), "폴백 타입 오류"
|
|
print(f" OK LLM 폴백 (None 반환): {type(result).__name__}")
|
|
except Exception as e:
|
|
print(f" ERR LLM 폴백: {type(e).__name__}: {e}")
|
|
|
|
asyncio.run(test_llm_fallback())
|
|
|
|
print("\n=== B-4 KB 자동 업데이트 에이전트 테스트 완료 ===")
|
|
if ok:
|
|
print("모든 검사 통과")
|
|
else:
|
|
sys.exit(1)
|