NinjainPJs commited on
Commit
b9da50c
·
1 Parent(s): babd3b4

Fix all ruff lint issues — 0 errors, 92 tests passing

Browse files
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 key, group in groups.items():
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
- f"| Severity | Count |",
135
- f"|----------|-------|",
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 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(
 
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, Depends, FastAPI, Header, HTTPException,
27
- Request, Response, Security,
 
 
 
 
 
 
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, _=Depends(verify_api_key)):
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, _=Depends(verify_api_key)):
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, Optional
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: Optional[str] = None
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: Optional[int] = None
34
- additions: Optional[int] = None
35
- deletions: Optional[int] = None
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: Optional[Installation] = None
 
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 AgentClass in [SecurityAgent, PerformanceAgent, StyleAgent]:
45
- agent = AgentClass()
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 MockChatGroq:
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
- MockChatGroq.return_value = mock_llm_instance
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 index_repo_files, _collection_name
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, BaseAgent, FindingOutput
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 MockChatGroq:
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
- MockChatGroq.return_value = mock_llm_instance
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 MockChatGroq:
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
- MockChatGroq.return_value = mock_llm_instance
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", None),
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()