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>
223 lines
8.4 KiB
Python
223 lines
8.4 KiB
Python
"""B-3 코드 리뷰 에이전트 테스트"""
|
|
import sys, ast, os, asyncio, json
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-b3-secret-key-32bytes-padded!")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_b3.db")
|
|
os.environ.setdefault("GUARDIA_PROJECTS_ROOT", r"C:\GUARDiA\projects")
|
|
|
|
print("=== 1. 구문 검사 ===")
|
|
files = ["core/code_review.py", "routers/code_review.py", "main.py"]
|
|
ok = True
|
|
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
|
|
if not ok:
|
|
sys.exit(1)
|
|
|
|
print("\n=== 2. models.py CodeReview 모델 확인 ===")
|
|
with open("models.py", encoding="utf-8") as f:
|
|
models_src = f.read()
|
|
|
|
checks = [
|
|
("class CodeReview(Base):", "CodeReview DB 모델"),
|
|
("class CodeReviewOut(BaseModel):", "CodeReviewOut Pydantic"),
|
|
("class CodeReviewRequest(BaseModel):", "CodeReviewRequest"),
|
|
("class ReviewSeverity(str, Enum):", "ReviewSeverity Enum"),
|
|
("class ReviewCategory(str, Enum):", "ReviewCategory Enum"),
|
|
("findings_json", "findings_json 컬럼"),
|
|
("project_dir", "project_dir 컬럼 (Project 모델)"),
|
|
("tech_stack", "tech_stack 컬럼"),
|
|
("last_review_score", "last_review_score 컬럼"),
|
|
]
|
|
for sym, desc in checks:
|
|
status = "OK" if sym in models_src else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 3. core/code_review.py 함수 확인 ===")
|
|
with open("core/code_review.py", encoding="utf-8") as f:
|
|
cr_src = f.read()
|
|
|
|
fn_checks = [
|
|
("def scan_source_files(", "파일 스캔 함수"),
|
|
("def detect_tech_stack(", "기술 스택 감지"),
|
|
("async def _call_ollama(", "Ollama API 호출"),
|
|
("def _build_review_prompt(", "프롬프트 생성"),
|
|
("def _parse_findings(", "findings 파싱"),
|
|
("def _calculate_score(", "점수 산출"),
|
|
("async def run_code_review(", "메인 리뷰 실행"),
|
|
("def quick_security_scan(", "빠른 보안 스캔"),
|
|
("SECURITY_PATTERNS", "보안 패턴 목록"),
|
|
("SKIP_DIRS", "제외 디렉토리 목록"),
|
|
]
|
|
for sym, desc in fn_checks:
|
|
status = "OK" if sym in cr_src else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 4. routers/code_review.py 엔드포인트 확인 ===")
|
|
with open("routers/code_review.py", encoding="utf-8") as f:
|
|
router_src = f.read()
|
|
|
|
endpoint_checks = [
|
|
('@router.post("", ', "POST /api/code-review (리뷰 요청)"),
|
|
('@router.get("/projects/list")', "GET /api/code-review/projects/list"),
|
|
('@router.get("/{review_id}", ', "GET /api/code-review/{id}"),
|
|
('@router.post("/quick-scan")', "POST /api/code-review/quick-scan"),
|
|
('@router.get("/{review_id}/findings")', "GET /api/code-review/{id}/findings"),
|
|
("BackgroundTasks", "비동기 백그라운드 실행"),
|
|
("_run_review_background", "백그라운드 실행 함수"),
|
|
]
|
|
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. projects/ 디렉토리 구조 확인 ===")
|
|
projects_root = Path(os.environ["GUARDIA_PROJECTS_ROOT"])
|
|
expected_projects = [
|
|
"testcase-java-api",
|
|
"testcase-py-api",
|
|
"testcase-js-frontend",
|
|
"testcase-php-legacy",
|
|
]
|
|
for proj in expected_projects:
|
|
status = "OK" if (projects_root / proj).exists() else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {proj}")
|
|
|
|
print("\n=== 6. scan_source_files 단위 테스트 ===")
|
|
try:
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("cr_mod", "core/code_review.py")
|
|
cr_mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(cr_mod)
|
|
|
|
for proj in expected_projects:
|
|
proj_path = projects_root / proj
|
|
if proj_path.exists():
|
|
files = cr_mod.scan_source_files(proj_path)
|
|
stack = cr_mod.detect_tech_stack(files)
|
|
print(f" OK {proj}: {len(files)}개 파일, 스택={stack}")
|
|
else:
|
|
print(f" SKIP {proj}: 경로 없음")
|
|
|
|
except Exception as e:
|
|
print(f" INFO 임포트 오류 (정상): {type(e).__name__}: {str(e)[:80]}")
|
|
|
|
print("\n=== 7. quick_security_scan 단위 테스트 ===")
|
|
async def test_quick_scan():
|
|
try:
|
|
from core.code_review import quick_security_scan, PROJECTS_ROOT
|
|
|
|
for proj in expected_projects:
|
|
proj_path = PROJECTS_ROOT / proj
|
|
if not proj_path.exists():
|
|
continue
|
|
findings = quick_security_scan(proj_path)
|
|
crit = sum(1 for f in findings if f["severity"] == "CRITICAL")
|
|
high = sum(1 for f in findings if f["severity"] == "HIGH")
|
|
print(f" OK {proj}: {len(findings)}건 발견 (CRITICAL={crit}, HIGH={high})")
|
|
|
|
except Exception as e:
|
|
print(f" INFO 스캔 오류: {type(e).__name__}: {str(e)[:80]}")
|
|
|
|
asyncio.run(test_quick_scan())
|
|
|
|
print("\n=== 8. 점수 산출 로직 테스트 ===")
|
|
try:
|
|
from core.code_review import _calculate_score
|
|
|
|
cases = [
|
|
([], 95, "빈 findings"),
|
|
([{"severity": "INFO"}] * 5, 95, "INFO만 5건"),
|
|
([{"severity": "LOW"}] * 3, 94, "LOW 3건 (6점 감점)"),
|
|
([{"severity": "MEDIUM"}] * 4, 80, "MEDIUM 4건 (20점 감점)"),
|
|
([{"severity": "HIGH"}] * 3, 70, "HIGH 3건 (30점 감점)"),
|
|
([{"severity": "CRITICAL"}] * 2 + [{"severity": "HIGH"}] * 3, 30, "CRITICAL 2건 + HIGH 3건"),
|
|
]
|
|
for findings, expected, label in cases:
|
|
score = _calculate_score(findings)
|
|
status = "OK" if score == expected else f"WARN(got {score}, expected {expected})"
|
|
print(f" {status} {label}: score={score}")
|
|
|
|
except Exception as e:
|
|
print(f" INFO 점수 계산 오류: {type(e).__name__}: {str(e)[:80]}")
|
|
|
|
print("\n=== 9. findings 파싱 테스트 ===")
|
|
try:
|
|
from core.code_review import _parse_findings
|
|
|
|
valid_json = '''[
|
|
{"severity": "CRITICAL", "category": "SECURITY", "line": 42,
|
|
"message": "SQL 인젝션 취약점", "suggestion": "PreparedStatement 사용"},
|
|
{"severity": "HIGH", "category": "CODE_QUALITY", "line": null,
|
|
"message": "null 반환", "suggestion": "Optional 사용"}
|
|
]'''
|
|
result = _parse_findings(valid_json, "test/File.java")
|
|
assert len(result) == 2
|
|
assert result[0]["severity"] == "CRITICAL"
|
|
assert result[0]["file"] == "test/File.java"
|
|
print(" OK 유효한 JSON 파싱")
|
|
|
|
result2 = _parse_findings("LLM이 설명을 길게 써서... []", "test/File.java")
|
|
assert result2 == []
|
|
print(" OK 빈 배열 파싱")
|
|
|
|
result3 = _parse_findings("완전 잘못된 응답", "test/File.java")
|
|
assert result3 == []
|
|
print(" OK 잘못된 응답 파싱 (빈 배열 반환)")
|
|
|
|
except Exception as e:
|
|
print(f" ERR findings 파싱 오류: {type(e).__name__}: {e}")
|
|
ok = False
|
|
|
|
print("\n=== 10. 하네스 구조 확인 ===")
|
|
harness_checks = [
|
|
(r"C:\GUARDiA\itsm\.claude\agents\sr-manager.md", "SR 매니저 에이전트"),
|
|
(r"C:\GUARDiA\itsm\.claude\agents\code-reviewer.md", "코드 리뷰 에이전트"),
|
|
(r"C:\GUARDiA\itsm\.claude\agents\deploy-engineer.md", "배포 엔지니어 에이전트"),
|
|
(r"C:\GUARDiA\itsm\.claude\agents\sla-guardian.md", "SLA 가디언 에이전트"),
|
|
(r"C:\GUARDiA\itsm\.claude\agents\incident-responder.md", "인시던트 대응 에이전트"),
|
|
(r"C:\GUARDiA\itsm\.claude\skills\guardia-orchestrator\SKILL.md", "오케스트레이터 스킬"),
|
|
(r"C:\GUARDiA\itsm\.claude\skills\code-review\SKILL.md", "코드 리뷰 스킬"),
|
|
(r"C:\GUARDiA\itsm\.claude\skills\sr-lifecycle\SKILL.md", "SR 생명주기 스킬"),
|
|
(r"C:\GUARDiA\itsm\.claude\skills\deploy-pipeline\SKILL.md", "배포 파이프라인 스킬"),
|
|
]
|
|
for path, desc in harness_checks:
|
|
status = "OK" if Path(path).exists() else "ERR"
|
|
if status == "ERR":
|
|
ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 11. main.py 등록 확인 ===")
|
|
with open("main.py", encoding="utf-8") as f:
|
|
main_src = f.read()
|
|
|
|
main_checks = [
|
|
("code_review", "code_review 라우터 임포트"),
|
|
("code_review.router", "code_review 라우터 등록"),
|
|
]
|
|
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=== B-3 코드 리뷰 에이전트 테스트 완료 ===")
|
|
if ok:
|
|
print("모든 검사 통과")
|
|
else:
|
|
sys.exit(1)
|