Spaces:
Sleeping
Sleeping
Commit ·
b9da50c
1
Parent(s): babd3b4
Fix all ruff lint issues — 0 errors, 92 tests passing
Browse files- app/agents/synthesizer.py +1 -1
- app/context/retriever.py +1 -0
- app/db/postgres.py +0 -1
- app/github/comment_formatter.py +3 -3
- app/main.py +26 -20
- app/models/findings.py +2 -2
- app/models/webhook_payloads.py +4 -6
- dashboard/app/page.tsx +2 -2
- tests/eval/run_eval.py +1 -1
- tests/unit/test_parallel_agents.py +2 -3
- tests/unit/test_performance_agent.py +2 -3
- tests/unit/test_rag_pipeline.py +1 -2
- tests/unit/test_security_agent.py +4 -5
- tests/unit/test_style_agent.py +2 -3
- tests/unit/test_synthesizer.py +1 -2
- tests/unit/test_webhook_validation.py +0 -1
app/agents/synthesizer.py
CHANGED
|
@@ -87,7 +87,7 @@ def deduplicate_findings(findings: list[Finding]) -> list[Finding]:
|
|
| 87 |
deduped = []
|
| 88 |
duplicates_removed = 0
|
| 89 |
|
| 90 |
-
for
|
| 91 |
if len(group) == 1:
|
| 92 |
deduped.append(group[0])
|
| 93 |
continue
|
|
|
|
| 87 |
deduped = []
|
| 88 |
duplicates_removed = 0
|
| 89 |
|
| 90 |
+
for _key, group in groups.items():
|
| 91 |
if len(group) == 1:
|
| 92 |
deduped.append(group[0])
|
| 93 |
continue
|
app/context/retriever.py
CHANGED
|
@@ -85,6 +85,7 @@ async def retrieve_context(
|
|
| 85 |
results["documents"][0],
|
| 86 |
results["metadatas"][0],
|
| 87 |
results["distances"][0],
|
|
|
|
| 88 |
):
|
| 89 |
filepath = metadata.get("filepath", "unknown")
|
| 90 |
start = metadata.get("start_line", "?")
|
|
|
|
| 85 |
results["documents"][0],
|
| 86 |
results["metadatas"][0],
|
| 87 |
results["distances"][0],
|
| 88 |
+
strict=False,
|
| 89 |
):
|
| 90 |
filepath = metadata.get("filepath", "unknown")
|
| 91 |
start = metadata.get("start_line", "?")
|
app/db/postgres.py
CHANGED
|
@@ -14,7 +14,6 @@ Schema is auto-created on first connection via ensure_tables().
|
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
import json
|
| 17 |
-
from datetime import datetime, timezone
|
| 18 |
from uuid import uuid4
|
| 19 |
|
| 20 |
import structlog
|
|
|
|
| 14 |
from __future__ import annotations
|
| 15 |
|
| 16 |
import json
|
|
|
|
| 17 |
from uuid import uuid4
|
| 18 |
|
| 19 |
import structlog
|
app/github/comment_formatter.py
CHANGED
|
@@ -131,8 +131,8 @@ def format_summary_comment(review: SynthesizedReview) -> str:
|
|
| 131 |
"",
|
| 132 |
"### Findings Summary",
|
| 133 |
"",
|
| 134 |
-
|
| 135 |
-
|
| 136 |
f"| \U0001f6a8 Critical | {review.critical_count} |",
|
| 137 |
f"| \U0001f7e0 High | {review.high_count} |",
|
| 138 |
f"| \U0001f7e1 Medium | {review.medium_count} |",
|
|
@@ -162,7 +162,7 @@ def format_summary_comment(review: SynthesizedReview) -> str:
|
|
| 162 |
if review.findings:
|
| 163 |
lines.append("### Detailed Findings")
|
| 164 |
lines.append("")
|
| 165 |
-
for
|
| 166 |
severity_emoji = SEVERITY_EMOJI.get(finding.severity, "")
|
| 167 |
agent_emoji = AGENT_EMOJI.get(finding.agent, "")
|
| 168 |
lines.append(
|
|
|
|
| 131 |
"",
|
| 132 |
"### Findings Summary",
|
| 133 |
"",
|
| 134 |
+
"| Severity | Count |",
|
| 135 |
+
"|----------|-------|",
|
| 136 |
f"| \U0001f6a8 Critical | {review.critical_count} |",
|
| 137 |
f"| \U0001f7e0 High | {review.high_count} |",
|
| 138 |
f"| \U0001f7e1 Medium | {review.medium_count} |",
|
|
|
|
| 162 |
if review.findings:
|
| 163 |
lines.append("### Detailed Findings")
|
| 164 |
lines.append("")
|
| 165 |
+
for _i, finding in enumerate(review.findings, 1):
|
| 166 |
severity_emoji = SEVERITY_EMOJI.get(finding.severity, "")
|
| 167 |
agent_emoji = AGENT_EMOJI.get(finding.agent, "")
|
| 168 |
lines.append(
|
app/main.py
CHANGED
|
@@ -22,32 +22,25 @@ import asyncio
|
|
| 22 |
import json
|
| 23 |
import traceback
|
| 24 |
|
|
|
|
| 25 |
from fastapi import (
|
| 26 |
-
BackgroundTasks,
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
)
|
| 29 |
from fastapi.middleware.cors import CORSMiddleware
|
| 30 |
from fastapi.security import APIKeyHeader
|
| 31 |
-
import structlog
|
| 32 |
-
|
| 33 |
-
from app.config import settings
|
| 34 |
-
|
| 35 |
-
# ── API Key auth for dashboard endpoints ──────────────────────────────────
|
| 36 |
-
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
async def verify_api_key(api_key: str = Security(_api_key_header)):
|
| 40 |
-
"""Reject dashboard API requests that don't carry a valid API key."""
|
| 41 |
-
if not settings.dashboard_api_key:
|
| 42 |
-
return # No key configured → allow (dev mode)
|
| 43 |
-
if api_key != settings.dashboard_api_key:
|
| 44 |
-
raise HTTPException(status_code=403, detail="Invalid or missing API key")
|
| 45 |
-
|
| 46 |
|
| 47 |
from app.agents.performance_agent import PerformanceAgent
|
| 48 |
from app.agents.security_agent import SecurityAgent
|
| 49 |
from app.agents.style_agent import StyleAgent
|
| 50 |
from app.agents.synthesizer import synthesize
|
|
|
|
| 51 |
from app.context.indexer import index_repo_files
|
| 52 |
from app.context.retriever import retrieve_context
|
| 53 |
from app.db.postgres import save_review
|
|
@@ -55,11 +48,24 @@ from app.db.redis_cache import is_already_reviewed, mark_as_reviewed
|
|
| 55 |
from app.github.client import GitHubClient
|
| 56 |
from app.github.comment_formatter import (
|
| 57 |
findings_to_review_comments,
|
| 58 |
-
format_inline_comment,
|
| 59 |
format_summary_comment,
|
| 60 |
)
|
| 61 |
from app.github.webhook import validate_webhook_signature
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
logger = structlog.get_logger()
|
| 64 |
|
| 65 |
_is_production = settings.environment == "production"
|
|
@@ -108,7 +114,7 @@ async def health_check():
|
|
| 108 |
|
| 109 |
|
| 110 |
@app.get("/api/repos/{owner}/{repo}/reviews")
|
| 111 |
-
async def get_reviews(owner: str, repo: str, _=
|
| 112 |
"""Get recent PR reviews for a repo (used by dashboard)."""
|
| 113 |
from app.db.postgres import get_repo_reviews
|
| 114 |
repo_full_name = f"{owner}/{repo}"
|
|
@@ -117,7 +123,7 @@ async def get_reviews(owner: str, repo: str, _=Depends(verify_api_key)):
|
|
| 117 |
|
| 118 |
|
| 119 |
@app.get("/api/repos/{owner}/{repo}/stats")
|
| 120 |
-
async def get_stats(owner: str, repo: str, _=
|
| 121 |
"""Get aggregate stats for a repo (used by dashboard)."""
|
| 122 |
from app.db.postgres import get_repo_reviews
|
| 123 |
repo_full_name = f"{owner}/{repo}"
|
|
|
|
| 22 |
import json
|
| 23 |
import traceback
|
| 24 |
|
| 25 |
+
import structlog
|
| 26 |
from fastapi import (
|
| 27 |
+
BackgroundTasks,
|
| 28 |
+
Depends,
|
| 29 |
+
FastAPI,
|
| 30 |
+
Header,
|
| 31 |
+
HTTPException,
|
| 32 |
+
Request,
|
| 33 |
+
Response,
|
| 34 |
+
Security,
|
| 35 |
)
|
| 36 |
from fastapi.middleware.cors import CORSMiddleware
|
| 37 |
from fastapi.security import APIKeyHeader
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
from app.agents.performance_agent import PerformanceAgent
|
| 40 |
from app.agents.security_agent import SecurityAgent
|
| 41 |
from app.agents.style_agent import StyleAgent
|
| 42 |
from app.agents.synthesizer import synthesize
|
| 43 |
+
from app.config import settings
|
| 44 |
from app.context.indexer import index_repo_files
|
| 45 |
from app.context.retriever import retrieve_context
|
| 46 |
from app.db.postgres import save_review
|
|
|
|
| 48 |
from app.github.client import GitHubClient
|
| 49 |
from app.github.comment_formatter import (
|
| 50 |
findings_to_review_comments,
|
|
|
|
| 51 |
format_summary_comment,
|
| 52 |
)
|
| 53 |
from app.github.webhook import validate_webhook_signature
|
| 54 |
|
| 55 |
+
# ── API Key auth for dashboard endpoints ──────────────────────────────────
|
| 56 |
+
_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
async def verify_api_key(api_key: str = Security(_api_key_header)):
|
| 60 |
+
"""Reject dashboard API requests that don't carry a valid API key."""
|
| 61 |
+
if not settings.dashboard_api_key:
|
| 62 |
+
return
|
| 63 |
+
if api_key != settings.dashboard_api_key:
|
| 64 |
+
raise HTTPException(status_code=403, detail="Invalid or missing API key")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
_verify_api_key = Depends(verify_api_key)
|
| 68 |
+
|
| 69 |
logger = structlog.get_logger()
|
| 70 |
|
| 71 |
_is_production = settings.environment == "production"
|
|
|
|
| 114 |
|
| 115 |
|
| 116 |
@app.get("/api/repos/{owner}/{repo}/reviews")
|
| 117 |
+
async def get_reviews(owner: str, repo: str, _=_verify_api_key):
|
| 118 |
"""Get recent PR reviews for a repo (used by dashboard)."""
|
| 119 |
from app.db.postgres import get_repo_reviews
|
| 120 |
repo_full_name = f"{owner}/{repo}"
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
@app.get("/api/repos/{owner}/{repo}/stats")
|
| 126 |
+
async def get_stats(owner: str, repo: str, _=_verify_api_key):
|
| 127 |
"""Get aggregate stats for a repo (used by dashboard)."""
|
| 128 |
from app.db.postgres import get_repo_reviews
|
| 129 |
repo_full_name = f"{owner}/{repo}"
|
app/models/findings.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from typing import Literal
|
| 6 |
from uuid import UUID, uuid4
|
| 7 |
|
| 8 |
from pydantic import BaseModel, Field
|
|
@@ -20,7 +20,7 @@ class Finding(BaseModel):
|
|
| 20 |
title: str
|
| 21 |
description: str
|
| 22 |
suggested_fix: str = ""
|
| 23 |
-
cwe_id:
|
| 24 |
confidence: float = Field(ge=0.0, le=1.0)
|
| 25 |
|
| 26 |
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
from typing import Literal
|
| 6 |
from uuid import UUID, uuid4
|
| 7 |
|
| 8 |
from pydantic import BaseModel, Field
|
|
|
|
| 20 |
title: str
|
| 21 |
description: str
|
| 22 |
suggested_fix: str = ""
|
| 23 |
+
cwe_id: str | None = None
|
| 24 |
confidence: float = Field(ge=0.0, le=1.0)
|
| 25 |
|
| 26 |
|
app/models/webhook_payloads.py
CHANGED
|
@@ -2,8 +2,6 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
from typing import Optional
|
| 6 |
-
|
| 7 |
from pydantic import BaseModel
|
| 8 |
|
| 9 |
|
|
@@ -30,9 +28,9 @@ class PullRequest(BaseModel):
|
|
| 30 |
state: str
|
| 31 |
head: PullRequestHead
|
| 32 |
draft: bool = False
|
| 33 |
-
changed_files:
|
| 34 |
-
additions:
|
| 35 |
-
deletions:
|
| 36 |
|
| 37 |
|
| 38 |
class PullRequestEvent(BaseModel):
|
|
@@ -52,4 +50,4 @@ class Installation(BaseModel):
|
|
| 52 |
class PullRequestEventWithInstallation(PullRequestEvent):
|
| 53 |
"""Pull request event with GitHub App installation context."""
|
| 54 |
|
| 55 |
-
installation:
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
|
|
|
| 5 |
from pydantic import BaseModel
|
| 6 |
|
| 7 |
|
|
|
|
| 28 |
state: str
|
| 29 |
head: PullRequestHead
|
| 30 |
draft: bool = False
|
| 31 |
+
changed_files: int | None = None
|
| 32 |
+
additions: int | None = None
|
| 33 |
+
deletions: int | None = None
|
| 34 |
|
| 35 |
|
| 36 |
class PullRequestEvent(BaseModel):
|
|
|
|
| 50 |
class PullRequestEventWithInstallation(PullRequestEvent):
|
| 51 |
"""Pull request event with GitHub App installation context."""
|
| 52 |
|
| 53 |
+
installation: Installation | None = None
|
dashboard/app/page.tsx
CHANGED
|
@@ -261,8 +261,8 @@ export default function HomePage() {
|
|
| 261 |
|
| 262 |
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
| 263 |
{AGENTS.map((agent) => (
|
| 264 |
-
<StaggerItem key={agent.title}>
|
| 265 |
-
<HoverCard>
|
| 266 |
<div
|
| 267 |
className={`glass rounded-2xl p-6 border ${agent.border} transition-all duration-300 h-full`}
|
| 268 |
>
|
|
|
|
| 261 |
|
| 262 |
<StaggerContainer className="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
| 263 |
{AGENTS.map((agent) => (
|
| 264 |
+
<StaggerItem key={agent.title} className="h-full">
|
| 265 |
+
<HoverCard className="h-full">
|
| 266 |
<div
|
| 267 |
className={`glass rounded-2xl p-6 border ${agent.border} transition-all duration-300 h-full`}
|
| 268 |
>
|
tests/eval/run_eval.py
CHANGED
|
@@ -36,8 +36,8 @@ async def evaluate_single_pr(test_case: dict) -> EvalResult:
|
|
| 36 |
A finding is considered a true positive if it matches an expected
|
| 37 |
finding on the same file_path and within 3 lines of the expected line.
|
| 38 |
"""
|
| 39 |
-
from app.agents.security_agent import SecurityAgent
|
| 40 |
from app.agents.performance_agent import PerformanceAgent
|
|
|
|
| 41 |
from app.agents.style_agent import StyleAgent
|
| 42 |
from app.agents.synthesizer import synthesize
|
| 43 |
from app.github.client import PRData
|
|
|
|
| 36 |
A finding is considered a true positive if it matches an expected
|
| 37 |
finding on the same file_path and within 3 lines of the expected line.
|
| 38 |
"""
|
|
|
|
| 39 |
from app.agents.performance_agent import PerformanceAgent
|
| 40 |
+
from app.agents.security_agent import SecurityAgent
|
| 41 |
from app.agents.style_agent import StyleAgent
|
| 42 |
from app.agents.synthesizer import synthesize
|
| 43 |
from app.github.client import PRData
|
tests/unit/test_parallel_agents.py
CHANGED
|
@@ -25,7 +25,6 @@ from app.agents.performance_agent import PerformanceAgent
|
|
| 25 |
from app.agents.security_agent import SecurityAgent
|
| 26 |
from app.agents.style_agent import StyleAgent
|
| 27 |
|
| 28 |
-
|
| 29 |
# ─── Agent Identity Tests ─────────────────────────────────────────────────
|
| 30 |
|
| 31 |
|
|
@@ -41,8 +40,8 @@ class TestAgentIdentities:
|
|
| 41 |
|
| 42 |
def test_all_agents_load_prompts(self):
|
| 43 |
"""Each agent should load its system prompt without errors."""
|
| 44 |
-
for
|
| 45 |
-
agent =
|
| 46 |
prompt = agent.system_prompt
|
| 47 |
assert len(prompt) > 100, f"{agent.agent_name} prompt is too short"
|
| 48 |
|
|
|
|
| 25 |
from app.agents.security_agent import SecurityAgent
|
| 26 |
from app.agents.style_agent import StyleAgent
|
| 27 |
|
|
|
|
| 28 |
# ─── Agent Identity Tests ─────────────────────────────────────────────────
|
| 29 |
|
| 30 |
|
|
|
|
| 40 |
|
| 41 |
def test_all_agents_load_prompts(self):
|
| 42 |
"""Each agent should load its system prompt without errors."""
|
| 43 |
+
for agent_class in [SecurityAgent, PerformanceAgent, StyleAgent]:
|
| 44 |
+
agent = agent_class()
|
| 45 |
prompt = agent.system_prompt
|
| 46 |
assert len(prompt) > 100, f"{agent.agent_name} prompt is too short"
|
| 47 |
|
tests/unit/test_performance_agent.py
CHANGED
|
@@ -23,7 +23,6 @@ from app.agents.performance_agent import PerformanceAgent
|
|
| 23 |
from app.github.client import PRData
|
| 24 |
from app.tools.radon_tool import run_radon
|
| 25 |
|
| 26 |
-
|
| 27 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 28 |
|
| 29 |
|
|
@@ -120,13 +119,13 @@ class TestPerformanceAgent:
|
|
| 120 |
"""LLM failure should return empty list, not crash."""
|
| 121 |
mock_chain = AsyncMock(side_effect=Exception("Groq rate limit"))
|
| 122 |
|
| 123 |
-
with patch("app.agents.base_agent.ChatGroq") as
|
| 124 |
mock_llm_instance = MagicMock()
|
| 125 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 126 |
__ror__=MagicMock(return_value=mock_chain),
|
| 127 |
__or__=MagicMock(return_value=mock_chain),
|
| 128 |
)
|
| 129 |
-
|
| 130 |
|
| 131 |
agent = PerformanceAgent()
|
| 132 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
|
|
|
| 23 |
from app.github.client import PRData
|
| 24 |
from app.tools.radon_tool import run_radon
|
| 25 |
|
|
|
|
| 26 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 27 |
|
| 28 |
|
|
|
|
| 119 |
"""LLM failure should return empty list, not crash."""
|
| 120 |
mock_chain = AsyncMock(side_effect=Exception("Groq rate limit"))
|
| 121 |
|
| 122 |
+
with patch("app.agents.base_agent.ChatGroq") as mock_chat_groq:
|
| 123 |
mock_llm_instance = MagicMock()
|
| 124 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 125 |
__ror__=MagicMock(return_value=mock_chain),
|
| 126 |
__or__=MagicMock(return_value=mock_chain),
|
| 127 |
)
|
| 128 |
+
mock_chat_groq.return_value = mock_llm_instance
|
| 129 |
|
| 130 |
agent = PerformanceAgent()
|
| 131 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
tests/unit/test_rag_pipeline.py
CHANGED
|
@@ -16,10 +16,9 @@ from unittest.mock import patch
|
|
| 16 |
import pytest
|
| 17 |
|
| 18 |
from app.context.embedder import chunk_code
|
| 19 |
-
from app.context.indexer import
|
| 20 |
from app.context.retriever import retrieve_context
|
| 21 |
|
| 22 |
-
|
| 23 |
# ─── Code Chunking Tests ─────────────────────────────────────────────────
|
| 24 |
|
| 25 |
|
|
|
|
| 16 |
import pytest
|
| 17 |
|
| 18 |
from app.context.embedder import chunk_code
|
| 19 |
+
from app.context.indexer import _collection_name, index_repo_files
|
| 20 |
from app.context.retriever import retrieve_context
|
| 21 |
|
|
|
|
| 22 |
# ─── Code Chunking Tests ─────────────────────────────────────────────────
|
| 23 |
|
| 24 |
|
tests/unit/test_security_agent.py
CHANGED
|
@@ -18,18 +18,17 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
| 18 |
|
| 19 |
import pytest
|
| 20 |
|
| 21 |
-
from app.agents.base_agent import AgentFindings,
|
| 22 |
from app.agents.security_agent import SecurityAgent
|
| 23 |
from app.github.client import PRData
|
| 24 |
from app.github.comment_formatter import (
|
|
|
|
| 25 |
format_inline_comment,
|
| 26 |
format_summary_comment,
|
| 27 |
-
findings_to_review_comments,
|
| 28 |
)
|
| 29 |
from app.models.findings import Finding, SynthesizedReview
|
| 30 |
from app.tools.bandit_tool import run_bandit
|
| 31 |
|
| 32 |
-
|
| 33 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 34 |
|
| 35 |
|
|
@@ -164,13 +163,13 @@ class TestSecurityAgent:
|
|
| 164 |
# Patch at the class level since ChatGroq is a Pydantic model
|
| 165 |
mock_chain = AsyncMock(side_effect=Exception("Groq API timeout"))
|
| 166 |
|
| 167 |
-
with patch("app.agents.base_agent.ChatGroq") as
|
| 168 |
mock_llm_instance = MagicMock()
|
| 169 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 170 |
__ror__=MagicMock(return_value=mock_chain),
|
| 171 |
__or__=MagicMock(return_value=mock_chain),
|
| 172 |
)
|
| 173 |
-
|
| 174 |
|
| 175 |
agent = SecurityAgent()
|
| 176 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
|
|
|
| 18 |
|
| 19 |
import pytest
|
| 20 |
|
| 21 |
+
from app.agents.base_agent import AgentFindings, FindingOutput
|
| 22 |
from app.agents.security_agent import SecurityAgent
|
| 23 |
from app.github.client import PRData
|
| 24 |
from app.github.comment_formatter import (
|
| 25 |
+
findings_to_review_comments,
|
| 26 |
format_inline_comment,
|
| 27 |
format_summary_comment,
|
|
|
|
| 28 |
)
|
| 29 |
from app.models.findings import Finding, SynthesizedReview
|
| 30 |
from app.tools.bandit_tool import run_bandit
|
| 31 |
|
|
|
|
| 32 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 33 |
|
| 34 |
|
|
|
|
| 163 |
# Patch at the class level since ChatGroq is a Pydantic model
|
| 164 |
mock_chain = AsyncMock(side_effect=Exception("Groq API timeout"))
|
| 165 |
|
| 166 |
+
with patch("app.agents.base_agent.ChatGroq") as mock_chat_groq:
|
| 167 |
mock_llm_instance = MagicMock()
|
| 168 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 169 |
__ror__=MagicMock(return_value=mock_chain),
|
| 170 |
__or__=MagicMock(return_value=mock_chain),
|
| 171 |
)
|
| 172 |
+
mock_chat_groq.return_value = mock_llm_instance
|
| 173 |
|
| 174 |
agent = SecurityAgent()
|
| 175 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
tests/unit/test_style_agent.py
CHANGED
|
@@ -22,7 +22,6 @@ from app.agents.style_agent import StyleAgent
|
|
| 22 |
from app.github.client import PRData
|
| 23 |
from app.tools.linter_tool import run_ruff
|
| 24 |
|
| 25 |
-
|
| 26 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 27 |
|
| 28 |
|
|
@@ -135,13 +134,13 @@ class TestStyleAgent:
|
|
| 135 |
"""LLM failure should return empty list, not crash."""
|
| 136 |
mock_chain = AsyncMock(side_effect=Exception("Groq API timeout"))
|
| 137 |
|
| 138 |
-
with patch("app.agents.base_agent.ChatGroq") as
|
| 139 |
mock_llm_instance = MagicMock()
|
| 140 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 141 |
__ror__=MagicMock(return_value=mock_chain),
|
| 142 |
__or__=MagicMock(return_value=mock_chain),
|
| 143 |
)
|
| 144 |
-
|
| 145 |
|
| 146 |
agent = StyleAgent()
|
| 147 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
|
|
|
| 22 |
from app.github.client import PRData
|
| 23 |
from app.tools.linter_tool import run_ruff
|
| 24 |
|
|
|
|
| 25 |
# ─── Fixtures ──────────────────────────────────────────────────────────────
|
| 26 |
|
| 27 |
|
|
|
|
| 134 |
"""LLM failure should return empty list, not crash."""
|
| 135 |
mock_chain = AsyncMock(side_effect=Exception("Groq API timeout"))
|
| 136 |
|
| 137 |
+
with patch("app.agents.base_agent.ChatGroq") as mock_chat_groq:
|
| 138 |
mock_llm_instance = MagicMock()
|
| 139 |
mock_llm_instance.with_structured_output.return_value = MagicMock(
|
| 140 |
__ror__=MagicMock(return_value=mock_chain),
|
| 141 |
__or__=MagicMock(return_value=mock_chain),
|
| 142 |
)
|
| 143 |
+
mock_chat_groq.return_value = mock_llm_instance
|
| 144 |
|
| 145 |
agent = StyleAgent()
|
| 146 |
with patch.object(agent, "run_static_analysis", return_value=""):
|
tests/unit/test_synthesizer.py
CHANGED
|
@@ -10,7 +10,6 @@ These tests verify:
|
|
| 10 |
6. Ranking puts critical findings first
|
| 11 |
"""
|
| 12 |
|
| 13 |
-
import pytest
|
| 14 |
|
| 15 |
from app.agents.synthesizer import (
|
| 16 |
deduplicate_findings,
|
|
@@ -35,7 +34,7 @@ def _make_finding(agent="security", severity="high", file_path="app.py",
|
|
| 35 |
title=kwargs.get("title", f"Test {category}"),
|
| 36 |
description=kwargs.get("description", "Test finding description."),
|
| 37 |
suggested_fix=kwargs.get("suggested_fix", ""),
|
| 38 |
-
cwe_id=kwargs.get("cwe_id"
|
| 39 |
confidence=confidence,
|
| 40 |
)
|
| 41 |
|
|
|
|
| 10 |
6. Ranking puts critical findings first
|
| 11 |
"""
|
| 12 |
|
|
|
|
| 13 |
|
| 14 |
from app.agents.synthesizer import (
|
| 15 |
deduplicate_findings,
|
|
|
|
| 34 |
title=kwargs.get("title", f"Test {category}"),
|
| 35 |
description=kwargs.get("description", "Test finding description."),
|
| 36 |
suggested_fix=kwargs.get("suggested_fix", ""),
|
| 37 |
+
cwe_id=kwargs.get("cwe_id"),
|
| 38 |
confidence=confidence,
|
| 39 |
)
|
| 40 |
|
tests/unit/test_webhook_validation.py
CHANGED
|
@@ -27,7 +27,6 @@ from fastapi.testclient import TestClient
|
|
| 27 |
|
| 28 |
from app.github.webhook import validate_webhook_signature
|
| 29 |
|
| 30 |
-
|
| 31 |
# Create a minimal FastAPI app just for testing the webhook dependency
|
| 32 |
# This isolates the test from the rest of the application
|
| 33 |
test_app = FastAPI()
|
|
|
|
| 27 |
|
| 28 |
from app.github.webhook import validate_webhook_signature
|
| 29 |
|
|
|
|
| 30 |
# Create a minimal FastAPI app just for testing the webhook dependency
|
| 31 |
# This isolates the test from the rest of the application
|
| 32 |
test_app = FastAPI()
|