refactor: replace Any types with specific types and update bad_code.md
- Replace Any with object or specific types across codebase - Add ReviewRequest dataclass for PR review payloads - Update bad_code.md: mark 5.1 (Any Type Overuse) as resolved - Fix summary table with accurate counts and unresolved issues list
This commit is contained in:
@@ -28,3 +28,4 @@ logs/
|
|||||||
agent_state.json
|
agent_state.json
|
||||||
ai-electronbun-todo-app/
|
ai-electronbun-todo-app/
|
||||||
test_connection.py
|
test_connection.py
|
||||||
|
.aider*
|
||||||
|
|||||||
+41
-15
@@ -218,9 +218,9 @@ Removed the `_configure_git_credentials()` method entirely. The agent now relies
|
|||||||
|
|
||||||
## 5. Code Quality Issues
|
## 5. Code Quality Issues
|
||||||
|
|
||||||
### 5.1 `Any` Type Overuse
|
### 5.1 `Any` Type Overuse [RESOLVED]
|
||||||
|
|
||||||
Throughout the codebase, `Any` is used where specific types would be better:
|
Throughout the codebase, `Any` was used where specific types would be better:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# gitea/client.py:34
|
# gitea/client.py:34
|
||||||
@@ -231,6 +231,17 @@ def get_authenticated_user(self) -> UserModel | None:
|
|||||||
def list_assigned_issues(self) -> list[dict]: # Bare dict, not dict[str, Any]
|
def list_assigned_issues(self) -> list[dict]: # Bare dict, not dict[str, Any]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
|
Replaced `Any` type annotations with `object` or specific types across the codebase:
|
||||||
|
- `core/agent.py`: Changed `Any` to `object` for message parameters and model attributes
|
||||||
|
- `gitea/files_client.py`: Changed `dict[str, Any]` to `dict[str, object]`
|
||||||
|
- `gitea/issues_client.py`: Changed `dict[str, Any]` to `dict[str, str | list[str]]` for issue data
|
||||||
|
- `gitea/models.py`: Changed `dict[str, Any]` to `dict[str, object]` for `head` and `base` fields
|
||||||
|
- `gitea/notifications_client.py`: Changed `list[dict[str, Any]]` to `list[dict[str, object]]`
|
||||||
|
- `gitea/prs_client.py`: Changed `dict[str, Any]` to specific types (`dict[str, str | None]`, `dict[str, str]`, `dict[str, object]`) and introduced `ReviewRequest` dataclass for review payloads
|
||||||
|
- `gitea/repos_client.py`: Changed `list[dict[str, Any]]` to `list[dict[str, object]]`
|
||||||
|
- `gitea/tools/git_tools.py`: Removed unused `from typing import Any` import
|
||||||
|
|
||||||
### 5.2 `assert` Used for Control Flow [RESOLVED]
|
### 5.2 `assert` Used for Control Flow [RESOLVED]
|
||||||
|
|
||||||
**Files:** `core/dispatcher.py:492, 741, 754`, `core/agent.py:74, 94`
|
**Files:** `core/dispatcher.py:492, 741, 754`, `core/agent.py:74, 94`
|
||||||
@@ -399,18 +410,33 @@ Removed the duplicate methods `add_comment` and `add_label` from `IssueTools`. O
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
| Category | Severity | Count |
|
| Category | Severity | Total | Unresolved |
|
||||||
| -------------------------- | -------- | ----- |
|
| -------------------------- | -------- | ----- | ---------- |
|
||||||
| Security Vulnerabilities | Critical | 0 |
|
| Security Vulnerabilities | Critical | 4 | 0 |
|
||||||
| Architecture Anti-Patterns | High | 3 |
|
| Architecture Anti-Patterns | High | 5 | 0 |
|
||||||
| Error Handling Problems | High | 0 |
|
| Error Handling Problems | High | 3 | 0 |
|
||||||
| Dangerous Side Effects | High | 0 |
|
| Dangerous Side Effects | High | 3 | 0 |
|
||||||
| Code Quality Issues | Medium | 3 |
|
| Code Quality Issues | Medium | 5 | 1 |
|
||||||
| Performance Issues | Medium | 1 |
|
| Performance Issues | Medium | 2 | 1 |
|
||||||
| Concurrency Issues | Medium | 1 |
|
| Concurrency Issues | Medium | 2 | 1 |
|
||||||
| Prompt Engineering Issues | Medium | 1 |
|
| Prompt Engineering Issues | Medium | 2 | 1 |
|
||||||
| Testing Issues | Medium | 2 |
|
| Testing Issues | Medium | 2 | 2 |
|
||||||
| CUPID Violations | High | 4 |
|
| CUPID Violations | High | 5 | 4 |
|
||||||
|
|
||||||
**Total: 29 issues identified, 8 unresolved.**
|
**Total: 33 issues identified, 10 unresolved.**
|
||||||
|
|
||||||
|
### Unresolved Issues
|
||||||
|
|
||||||
|
| # | Issue | Section |
|
||||||
|
| ---- | -------------------------------------- | ------- |
|
||||||
|
| 1 | Mutable Default Arguments (Near Miss) | 5.5 |
|
||||||
|
| 2 | No Caching | 6.2 |
|
||||||
|
| 3 | No Mutex on Workspace Operations | 7.2 |
|
||||||
|
| 4 | Massive Embedded System Prompts | 8.1 |
|
||||||
|
| 5 | Tests Don't Mock HTTP Calls | 9.1 |
|
||||||
|
| 6 | No Tests for Critical Paths | 9.2 |
|
||||||
|
| 7 | Not Understandable | 10.2 |
|
||||||
|
| 8 | Not Performant | 10.3 |
|
||||||
|
| 9 | Not Inspectable | 10.4 |
|
||||||
|
| 10 | Not Delightful | 10.5 |
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import lmstudio as lms
|
import lmstudio as lms
|
||||||
from typing import Any, Callable
|
from typing import Callable
|
||||||
from .prompt import CAVEMAN_PROMPT
|
from .prompt import CAVEMAN_PROMPT
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("agent-base")
|
logger: logging.Logger = logging.getLogger("agent-base")
|
||||||
@@ -13,7 +13,7 @@ class _ActResponseCapture:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.responses: list[str] = []
|
self.responses: list[str] = []
|
||||||
|
|
||||||
def __call__(self, message: Any) -> None:
|
def __call__(self, message: object) -> None:
|
||||||
content: str = ""
|
content: str = ""
|
||||||
if hasattr(message, 'content'):
|
if hasattr(message, 'content'):
|
||||||
content = message.content
|
content = message.content
|
||||||
@@ -58,7 +58,7 @@ class BaseAgent:
|
|||||||
|
|
||||||
def __init__(self, model_name: str) -> None:
|
def __init__(self, model_name: str) -> None:
|
||||||
self.model_name: str = model_name
|
self.model_name: str = model_name
|
||||||
self.model: Any | None = None
|
self.model: object | None = None
|
||||||
self.system_prompt: str = ""
|
self.system_prompt: str = ""
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@@ -87,7 +87,7 @@ class BaseAgent:
|
|||||||
logger.error(f"Agent execution error: {e}")
|
logger.error(f"Agent execution error: {e}")
|
||||||
return f"Error in agent execution: {str(e)}"
|
return f"Error in agent execution: {str(e)}"
|
||||||
|
|
||||||
async def run_with_tools(self, user_input: str, tools: list[Any]) -> str:
|
async def run_with_tools(self, user_input: str, tools: list[object]) -> str:
|
||||||
"""Run the agent with tool calling capability."""
|
"""Run the agent with tool calling capability."""
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
await self.initialize()
|
await self.initialize()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ class FilesClient:
|
|||||||
|
|
||||||
def update_file(
|
def update_file(
|
||||||
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, object]:
|
||||||
"""Update a file in a repository.
|
"""Update a file in a repository.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -79,7 +78,7 @@ class FilesClient:
|
|||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, object]:
|
||||||
"""Update a git reference.
|
"""Update a git reference.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -97,7 +96,7 @@ class FilesClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, object]:
|
||||||
"""Create a new git reference.
|
"""Create a new git reference.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Issues client for Gitea API operations."""
|
"""Issues client for Gitea API operations."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ class IssuesClient:
|
|||||||
The created issue.
|
The created issue.
|
||||||
"""
|
"""
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues"
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues"
|
||||||
data: dict[str, Any] = {"title": title, "body": body}
|
data: dict[str, str | list[str]] = {"title": title, "body": body}
|
||||||
if labels:
|
if labels:
|
||||||
data["labels"] = labels
|
data["labels"] = labels
|
||||||
if assignees:
|
if assignees:
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
"""Pydantic models for Gitea API entities."""
|
"""Pydantic models for Gitea API entities."""
|
||||||
|
|
||||||
from typing import Optional, Any
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ class PullRequestModel(BaseModel):
|
|||||||
updated_at: Optional[str] = None
|
updated_at: Optional[str] = None
|
||||||
closed_at: Optional[str] = None
|
closed_at: Optional[str] = None
|
||||||
merged_at: Optional[str] = None
|
merged_at: Optional[str] = None
|
||||||
head: dict[str, Any] = Field(default_factory=dict)
|
head: dict[str, object] = Field(default_factory=dict)
|
||||||
base: dict[str, Any] = Field(default_factory=dict)
|
base: dict[str, object] = Field(default_factory=dict)
|
||||||
repository: Optional[RepositoryModel] = None
|
repository: Optional[RepositoryModel] = None
|
||||||
comments: int = 0
|
comments: int = 0
|
||||||
comments_url: Optional[str] = None
|
comments_url: Optional[str] = None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Notifications client for Gitea API operations."""
|
"""Notifications client for Gitea API operations."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class NotificationsClient:
|
|||||||
|
|
||||||
def list_unread_notifications(
|
def list_unread_notifications(
|
||||||
self, since: Optional[str] = None
|
self, since: Optional[str] = None
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, object]]:
|
||||||
"""List unread notifications.
|
"""List unread notifications.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -42,9 +42,9 @@ class NotificationsClient:
|
|||||||
params["since"] = since
|
params["since"] = since
|
||||||
response = self.client.get(url, params=params)
|
response = self.client.get(url, params=params)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
notifications: list[dict[str, Any]] = response.json()
|
notifications: list[dict[str, object]] = response.json()
|
||||||
|
|
||||||
result: list[dict[str, Any]] = []
|
result: list[dict[str, object]] = []
|
||||||
for n in notifications:
|
for n in notifications:
|
||||||
repo_info = n.get("repository") or {}
|
repo_info = n.get("repository") or {}
|
||||||
owner_info = repo_info.get("owner") or {}
|
owner_info = repo_info.get("owner") or {}
|
||||||
|
|||||||
+18
-11
@@ -1,7 +1,8 @@
|
|||||||
"""Pull Requests client for Gitea API operations."""
|
"""Pull Requests client for Gitea API operations."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -17,6 +18,12 @@ from .models import (
|
|||||||
logger: logging.Logger = logging.getLogger("gitea.prs_client")
|
logger: logging.Logger = logging.getLogger("gitea.prs_client")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ReviewRequest:
|
||||||
|
event: str
|
||||||
|
body: str
|
||||||
|
|
||||||
|
|
||||||
class PullRequestsClient:
|
class PullRequestsClient:
|
||||||
"""HTTP client for Gitea Pull Requests API operations."""
|
"""HTTP client for Gitea Pull Requests API operations."""
|
||||||
|
|
||||||
@@ -288,7 +295,7 @@ class PullRequestsClient:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
||||||
data: dict[str, Any] = {}
|
data: dict[str, str | None] = {}
|
||||||
if title is not None:
|
if title is not None:
|
||||||
data["title"] = title
|
data["title"] = title
|
||||||
if body is not None:
|
if body is not None:
|
||||||
@@ -322,7 +329,7 @@ class PullRequestsClient:
|
|||||||
|
|
||||||
def approve_pr(
|
def approve_pr(
|
||||||
self, owner: str, repo: str, pr_number: int, comment: str
|
self, owner: str, repo: str, pr_number: int, comment: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, object]:
|
||||||
"""Approve a pull request.
|
"""Approve a pull request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -335,14 +342,14 @@ class PullRequestsClient:
|
|||||||
The review response.
|
The review response.
|
||||||
"""
|
"""
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
data: dict[str, Any] = {"event": "APPROVED", "body": comment}
|
review: ReviewRequest = ReviewRequest(event="APPROVED", body=comment)
|
||||||
response = self.client.post(url, json=data)
|
response = self.client.post(url, json={"event": review.event, "body": review.body})
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def request_changes_pr(
|
def request_changes_pr(
|
||||||
self, owner: str, repo: str, pr_number: int, comment: str
|
self, owner: str, repo: str, pr_number: int, comment: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, object]:
|
||||||
"""Request changes on a pull request.
|
"""Request changes on a pull request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -355,14 +362,14 @@ class PullRequestsClient:
|
|||||||
The review response.
|
The review response.
|
||||||
"""
|
"""
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
data: dict[str, Any] = {"event": "REQUEST_CHANGES", "body": comment}
|
review: ReviewRequest = ReviewRequest(event="REQUEST_CHANGES", body=comment)
|
||||||
response = self.client.post(url, json=data)
|
response = self.client.post(url, json={"event": review.event, "body": review.body})
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def get_pr_reviews(
|
def get_pr_reviews(
|
||||||
self, owner: str, repo: str, pr_number: int
|
self, owner: str, repo: str, pr_number: int
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, object]]:
|
||||||
"""Get reviews for a pull request.
|
"""Get reviews for a pull request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -382,7 +389,7 @@ class PullRequestsClient:
|
|||||||
|
|
||||||
def dismiss_review_pr(
|
def dismiss_review_pr(
|
||||||
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
|
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, object]:
|
||||||
"""Dismiss a review on a pull request.
|
"""Dismiss a review on a pull request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -447,7 +454,7 @@ class PullRequestsClient:
|
|||||||
url = (
|
url = (
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/merge"
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/merge"
|
||||||
)
|
)
|
||||||
data: dict[str, Any] = {
|
data: dict[str, str] = {
|
||||||
"Do": style,
|
"Do": style,
|
||||||
"MergeTitleField": title,
|
"MergeTitleField": title,
|
||||||
"MergeMessageField": message,
|
"MergeMessageField": message,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Repositories client for Gitea API operations."""
|
"""Repositories client for Gitea API operations."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ class ReposClient:
|
|||||||
url = f"{self.base_url}/api/v1/user/repos"
|
url = f"{self.base_url}/api/v1/user/repos"
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
repos: list[dict[str, Any]] = response.json()
|
repos: list[dict[str, object]] = response.json()
|
||||||
# Filter to ONLY configured organization repos, include mirrors
|
# Filter to ONLY configured organization repos, include mirrors
|
||||||
seen: set[str] = set()
|
seen: set[str] = set()
|
||||||
result: list[RepositoryModel] = []
|
result: list[RepositoryModel] = []
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Any
|
|
||||||
from gitea.client import GiteaClient
|
from gitea.client import GiteaClient
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from gitea.tools.coding_tools import CodingTools
|
|||||||
from core.dispatcher import AgentDispatcher
|
from core.dispatcher import AgentDispatcher
|
||||||
from core.queue import WorkItem
|
from core.queue import WorkItem
|
||||||
from gitea.client import GiteaClient
|
from gitea.client import GiteaClient
|
||||||
from gitea.tools.gitea_tools import GiteaTools
|
|
||||||
from gitea.models import IssueModel, PullRequestModel
|
from gitea.models import IssueModel, PullRequestModel
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
pytestmark = pytest.mark.anyio
|
||||||
@@ -96,12 +95,16 @@ def test_run_verification_failure(tmp_path: Path) -> None:
|
|||||||
@patch("core.dispatcher.CodingAgent")
|
@patch("core.dispatcher.CodingAgent")
|
||||||
@patch("core.dispatcher.PlanningAgent")
|
@patch("core.dispatcher.PlanningAgent")
|
||||||
async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMock, mock_coding_class: MagicMock) -> None:
|
async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMock, mock_coding_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
|
||||||
|
# Mock sub-clients
|
||||||
|
mock_client.prs = MagicMock()
|
||||||
|
mock_client.issues = MagicMock()
|
||||||
|
mock_client.notifications = MagicMock()
|
||||||
|
|
||||||
# Mock no existing PRs
|
# Mock no existing PRs
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
|
|
||||||
from gitea.models import UserModel
|
from gitea.models import UserModel
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
@@ -110,13 +113,14 @@ async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMoc
|
|||||||
number=42,
|
number=42,
|
||||||
title="fix bug",
|
title="fix bug",
|
||||||
body="bug details",
|
body="bug details",
|
||||||
user=UserModel(login="unknown-ai")
|
user=UserModel(login="unknown-ai"),
|
||||||
|
requested_reviewers=[UserModel(login="unknown-ai")],
|
||||||
)
|
)
|
||||||
mock_client.get_pull_request.return_value = mock_pr
|
mock_client.prs.get_pull_request.return_value = mock_pr
|
||||||
mock_client.get_pull_request_diff.return_value = "diff"
|
mock_client.prs.get_pull_request_diff.return_value = "diff"
|
||||||
mock_client.get_pull_request_comments.return_value = []
|
mock_client.prs.get_pull_request_comments.return_value = []
|
||||||
mock_client.get_pull_request_files.return_value = []
|
mock_client.prs.get_pull_request_files.return_value = []
|
||||||
mock_client.get_pr_reviews.return_value = []
|
mock_client.prs.get_pr_reviews.return_value = []
|
||||||
|
|
||||||
# Mock agent instances
|
# Mock agent instances
|
||||||
mock_planning_agent = MagicMock()
|
mock_planning_agent = MagicMock()
|
||||||
@@ -127,7 +131,18 @@ async def test_dispatch_planning_and_coding_phases(mock_planning_class: MagicMoc
|
|||||||
mock_coding_agent.run_with_tools = AsyncMock(return_value="PR #1 Created")
|
mock_coding_agent.run_with_tools = AsyncMock(return_value="PR #1 Created")
|
||||||
mock_coding_class.return_value = mock_coding_agent
|
mock_coding_class.return_value = mock_coding_agent
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
from gitea.tools.issue_tools import IssueTools
|
||||||
|
from gitea.tools.pr_tools import PRTools
|
||||||
|
from gitea.tools.file_tools import FileTools
|
||||||
|
from gitea.tools.git_tools import GitTools
|
||||||
|
|
||||||
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=IssueTools(mock_client),
|
||||||
|
pr_tools=PRTools(mock_client),
|
||||||
|
file_tools=FileTools(mock_client),
|
||||||
|
git_tools=GitTools(mock_client),
|
||||||
|
)
|
||||||
|
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
|
|||||||
+220
-108
@@ -2,16 +2,19 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, AsyncMock, patch, ANY
|
from unittest.mock import MagicMock, AsyncMock, patch, ANY
|
||||||
from core.dispatcher import AgentDispatcher
|
from core.dispatcher import AgentDispatcher
|
||||||
from core.queue import WorkItem
|
from core.queue import WorkItem
|
||||||
from gitea.client import GiteaClient
|
|
||||||
from gitea.tools.gitea_tools import GiteaTools
|
|
||||||
from gitea.models import PullRequestModel, IssueModel, CommentModel, UserModel
|
from gitea.models import PullRequestModel, IssueModel, CommentModel, UserModel
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
pytestmark = pytest.mark.anyio
|
||||||
|
|
||||||
|
|
||||||
async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
|
||||||
|
# Mock sub-clients
|
||||||
|
mock_client.prs = MagicMock()
|
||||||
|
mock_client.issues = MagicMock()
|
||||||
|
mock_client.notifications = MagicMock()
|
||||||
|
|
||||||
# Mock list_repo_pull_requests to return a PR that closes issue #42
|
# Mock list_repo_pull_requests to return a PR that closes issue #42
|
||||||
pr = PullRequestModel(
|
pr = PullRequestModel(
|
||||||
@@ -19,9 +22,15 @@ async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
|||||||
title="fix: resolve bug",
|
title="fix: resolve bug",
|
||||||
body="closes #42"
|
body="closes #42"
|
||||||
)
|
)
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr]
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
@@ -35,13 +44,14 @@ async def test_dispatch_skips_issue_with_existing_pr() -> None:
|
|||||||
|
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert "SKIP: A pull request (PR #101) addressing issue #42 already exists" in results[0]
|
assert "SKIP: A pull request (PR #101) addressing issue #42 already exists" in results[0]
|
||||||
mock_client.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
|
mock_client.prs.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
|
||||||
|
|
||||||
|
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock) -> None:
|
async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
# Mock list_repo_pull_requests to return PRs that don't address issue #42
|
# Mock list_repo_pull_requests to return PRs that don't address issue #42
|
||||||
pr = PullRequestModel(
|
pr = PullRequestModel(
|
||||||
@@ -49,8 +59,8 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
|
|||||||
title="feat: add something",
|
title="feat: add something",
|
||||||
body="closes #99"
|
body="closes #99"
|
||||||
)
|
)
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr]
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
|
|
||||||
# Mock CoordinatorAgent invoking propose_plan tool
|
# Mock CoordinatorAgent invoking propose_plan tool
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
@@ -61,7 +71,13 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
|
|||||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||||
mock_coord_class.return_value = mock_coord_instance
|
mock_coord_class.return_value = mock_coord_instance
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
@@ -79,8 +95,9 @@ async def test_dispatch_processes_issue_without_pr(mock_coord_class: MagicMock)
|
|||||||
|
|
||||||
@patch("core.dispatcher.CodingAgent")
|
@patch("core.dispatcher.CodingAgent")
|
||||||
async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMock) -> None:
|
async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
# Mock get_pull_request, get_pull_request_diff, etc.
|
# Mock get_pull_request, get_pull_request_diff, etc.
|
||||||
pr = PullRequestModel(
|
pr = PullRequestModel(
|
||||||
@@ -90,25 +107,31 @@ async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMoc
|
|||||||
head={"ref": "branch1"},
|
head={"ref": "branch1"},
|
||||||
base={"ref": "master"}
|
base={"ref": "master"}
|
||||||
)
|
)
|
||||||
mock_client.get_pull_request.return_value = pr
|
mock_client.prs.get_pull_request.return_value = pr
|
||||||
mock_client.get_pull_request_diff.return_value = "diff context"
|
mock_client.prs.get_pull_request_diff.return_value = "diff context"
|
||||||
mock_client.get_pull_request_files.return_value = []
|
mock_client.prs.get_pull_request_files.return_value = []
|
||||||
mock_client.get_pull_request_comments.return_value = []
|
mock_client.prs.get_pull_request_comments.return_value = []
|
||||||
|
|
||||||
# Mock the connected issue and its comments
|
# Mock the connected issue and its comments
|
||||||
issue = IssueModel(number=42, title="bug description")
|
issue = IssueModel(number=42, title="bug description")
|
||||||
mock_client.get_issue.return_value = issue
|
mock_client.issues.get_issue.return_value = issue
|
||||||
|
|
||||||
comment = CommentModel(id=1, body="First comment")
|
comment = CommentModel(id=1, body="First comment")
|
||||||
mock_client.get_issue_comments.return_value = [comment]
|
mock_client.issues.get_issue_comments.return_value = [comment]
|
||||||
|
|
||||||
# Mock CodingAgent
|
# Mock CodingAgent
|
||||||
mock_agent_instance = MagicMock()
|
mock_agent_instance = MagicMock()
|
||||||
mock_agent_instance.run_with_tools = AsyncMock(return_value="PR Reviewed.")
|
mock_agent_instance.run_with_tools = AsyncMock(return_value="PR Reviewed.")
|
||||||
mock_agent_class.return_value = mock_agent_instance
|
mock_agent_class.return_value = mock_agent_instance
|
||||||
|
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
@@ -126,46 +149,54 @@ async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMoc
|
|||||||
assert "bug description" in mission
|
assert "bug description" in mission
|
||||||
assert "First comment" in mission
|
assert "First comment" in mission
|
||||||
|
|
||||||
mock_client.get_issue.assert_called_once_with("meeks", "repo1", 42)
|
mock_client.issues.get_issue.assert_called_once_with("meeks", "repo1", 42)
|
||||||
mock_client.get_issue_comments.assert_called_once_with("meeks", "repo1", 42)
|
mock_client.issues.get_issue_comments.assert_called_once_with("meeks", "repo1", 42)
|
||||||
|
|
||||||
|
|
||||||
async def test_find_pr_for_issue_by_branch() -> None:
|
async def test_find_pr_for_issue_by_branch() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Matches fix/issue-42-some-desc
|
# 1. Matches fix/issue-42-some-desc
|
||||||
pr1 = PullRequestModel(number=102, head={"ref": "fix/issue-42-some-desc"})
|
pr1 = PullRequestModel(number=102, head={"ref": "fix/issue-42-some-desc"})
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr1]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr1]
|
||||||
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
||||||
|
|
||||||
# 2. Matches fix/42
|
# 2. Matches fix/42
|
||||||
pr2 = PullRequestModel(number=102, head={"ref": "fix/42"})
|
pr2 = PullRequestModel(number=102, head={"ref": "fix/42"})
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr2]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr2]
|
||||||
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
||||||
|
|
||||||
# 3. Matches fix-42_desc
|
# 3. Matches fix-42_desc
|
||||||
pr3 = PullRequestModel(number=102, head={"ref": "fix-42_desc"})
|
pr3 = PullRequestModel(number=102, head={"ref": "fix-42_desc"})
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr3]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr3]
|
||||||
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
|
||||||
|
|
||||||
# 4. Does NOT match fix/142
|
# 4. Does NOT match fix/142
|
||||||
pr4 = PullRequestModel(number=102, head={"ref": "fix/142"})
|
pr4 = PullRequestModel(number=102, head={"ref": "fix/142"})
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr4]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr4]
|
||||||
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
||||||
|
|
||||||
# 5. Does NOT match fix/421
|
# 5. Does NOT match fix/421
|
||||||
pr5 = PullRequestModel(number=102, head={"ref": "fix/421"})
|
pr5 = PullRequestModel(number=102, head={"ref": "fix/421"})
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr5]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr5]
|
||||||
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def test_find_pr_for_issue_by_raw_mention() -> None:
|
async def test_find_pr_for_issue_by_raw_mention() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
# PR body mentions #42
|
# PR body mentions #42
|
||||||
pr = PullRequestModel(
|
pr = PullRequestModel(
|
||||||
@@ -174,33 +205,51 @@ async def test_find_pr_for_issue_by_raw_mention() -> None:
|
|||||||
body="This is for #42 to fix the bug",
|
body="This is for #42 to fix the bug",
|
||||||
head={"ref": "some-branch"}
|
head={"ref": "some-branch"}
|
||||||
)
|
)
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr]
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
res = dispatcher._find_pr_for_issue("meeks/repo1", 42)
|
res = dispatcher._find_pr_for_issue("meeks/repo1", 42)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.number == 103
|
assert res.number == 103
|
||||||
|
|
||||||
|
|
||||||
async def test_dispatch_skips_already_reviewed_pr() -> None:
|
async def test_dispatch_skips_already_reviewed_pr() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
|
mock_client.prs = MagicMock()
|
||||||
|
mock_client.issues = MagicMock()
|
||||||
|
mock_client.notifications = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
from gitea.models import UserModel
|
from gitea.models import UserModel
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
pr = PullRequestModel(
|
pr = PullRequestModel(
|
||||||
number=104,
|
number=104,
|
||||||
title="already reviewed PR",
|
title="already reviewed PR",
|
||||||
body="closes #42",
|
body="closes #42",
|
||||||
user=UserModel(login="unknown-ai")
|
user=UserModel(login="unknown-ai"),
|
||||||
|
requested_reviewers=[],
|
||||||
)
|
)
|
||||||
mock_client.get_pull_request.return_value = pr
|
mock_client.prs.get_pull_request.return_value = pr
|
||||||
mock_client.get_pull_request_diff.return_value = "diff"
|
mock_client.prs.get_pull_request_diff.return_value = "diff"
|
||||||
mock_client.get_pull_request_comments.return_value = [
|
mock_client.prs.get_pull_request_comments.return_value = [
|
||||||
CommentModel(id=1, body="Reviewed by AI Agent: Looks good.")
|
CommentModel(id=1, body="Reviewed by AI Agent: Looks good.")
|
||||||
]
|
]
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
@@ -225,7 +274,7 @@ def _make_comment(login: str, body: str) -> CommentModel:
|
|||||||
|
|
||||||
def _make_dispatcher_for_reply_tests() -> AgentDispatcher:
|
def _make_dispatcher_for_reply_tests() -> AgentDispatcher:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
return AgentDispatcher(client=mock_client, tools=MagicMock())
|
return AgentDispatcher(client=mock_client, tools=MagicMock())
|
||||||
|
|
||||||
|
|
||||||
@@ -266,12 +315,13 @@ def test_is_awaiting_reply_no_marker_not_detected() -> None:
|
|||||||
|
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
|
async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
coord_tools.propose_plan(plan="- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
coord_tools.propose_plan(plan="- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
||||||
@@ -280,7 +330,13 @@ async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
|
|||||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||||
mock_coord_class.return_value = mock_coord_instance
|
mock_coord_class.return_value = mock_coord_instance
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -292,17 +348,18 @@ async def test_dispatch_proposes_plan(mock_coord_class: MagicMock) -> None:
|
|||||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
||||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Implementation Plan\n\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
|
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "### Proposed Implementation Plan\n\n- Add endpoint\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->\n\nIs this plan ok for implementation or do you have any comments/changes?\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->")
|
||||||
|
|
||||||
|
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
|
async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
coord_tools.answer_question(answer="X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
coord_tools.answer_question(answer="X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->", issue_number=42)
|
||||||
@@ -311,7 +368,13 @@ async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
|
|||||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||||
mock_coord_class.return_value = mock_coord_instance
|
mock_coord_class.return_value = mock_coord_instance
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -323,19 +386,20 @@ async def test_dispatch_answers_question(mock_coord_class: MagicMock) -> None:
|
|||||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert "POSTED_COMMENT: ANSWER_QUESTION" in results[0]
|
assert "POSTED_COMMENT: ANSWER_QUESTION" in results[0]
|
||||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->\n\nIs this answer satisfactory?\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
|
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "X works by doing Y.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->\n\nIs this answer satisfactory?\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->")
|
||||||
|
|
||||||
|
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock) -> None:
|
async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
# Human comments indicating satisfaction after our answer
|
# Human comments indicating satisfaction after our answer
|
||||||
mock_client.get_issue_comments.return_value = [
|
mock_client.issues.get_issue_comments.return_value = [
|
||||||
_make_comment("unknown-ai", "Here is the answer.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->"),
|
_make_comment("unknown-ai", "Here is the answer.\n<!-- agent:question-response -->\n<!-- agent:awaiting-reply -->"),
|
||||||
_make_comment("michael", "Yes, thanks! That makes sense.")
|
_make_comment("michael", "Yes, thanks! That makes sense.")
|
||||||
]
|
]
|
||||||
@@ -347,7 +411,13 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock
|
|||||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||||
mock_coord_class.return_value = mock_coord_instance
|
mock_coord_class.return_value = mock_coord_instance
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -359,27 +429,28 @@ async def test_dispatch_closes_issue_on_satisfaction(mock_coord_class: MagicMock
|
|||||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert "CLOSED_ISSUE: Issue #42 closed." in results[0]
|
assert "CLOSED_ISSUE: Issue #42 closed." in results[0]
|
||||||
mock_client.add_comment.assert_called_once_with("meeks", "repo1", 42, "Closing the issue now. Let me know if you need anything else!")
|
mock_client.issues.add_comment.assert_called_once_with("meeks", "repo1", 42, "Closing the issue now. Let me know if you need anything else!")
|
||||||
mock_client.close_issue.assert_called_once_with("meeks", "repo1", 42)
|
mock_client.issues.close_issue.assert_called_once_with("meeks", "repo1", 42)
|
||||||
|
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
@patch("core.dispatcher.CodingAgent")
|
@patch("core.dispatcher.CodingAgent")
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
mock_client.get_issue_comments.return_value = [
|
mock_client.issues.get_issue_comments.return_value = [
|
||||||
_make_comment("unknown-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
_make_comment("unknown-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
||||||
_make_comment("michael", "looks good, go ahead")
|
_make_comment("michael", "looks good, go ahead")
|
||||||
]
|
]
|
||||||
|
|
||||||
# Return PR object on creation
|
# Return PR object on creation
|
||||||
mock_pr = PullRequestModel(number=105, title="WIP: add X", html_url="http://gitea/pr/105", head={"ref": "fix/issue-42-add-x"})
|
mock_pr = PullRequestModel(number=105, title="WIP: add X", html_url="http://gitea/pr/105", head={"ref": "fix/issue-42-add-x"})
|
||||||
mock_client.create_pull_request.return_value = mock_pr
|
mock_client.prs.create_pull_request.return_value = mock_pr
|
||||||
|
|
||||||
# Mock planning agent deciding EXECUTE_PLAN
|
# Mock planning agent deciding EXECUTE_PLAN
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
@@ -390,7 +461,13 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_cla
|
|||||||
# Mock coding agent executing plan
|
# Mock coding agent executing plan
|
||||||
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
|
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Completed Successfully.")
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -408,20 +485,21 @@ async def test_dispatch_executes_approved_plan_and_creates_wip_pr(mock_coord_cla
|
|||||||
mock_run.assert_any_call(["git", "checkout", "-b", "fix/issue-42-add-x"], cwd=ANY, check=True)
|
mock_run.assert_any_call(["git", "checkout", "-b", "fix/issue-42-add-x"], cwd=ANY, check=True)
|
||||||
|
|
||||||
# Verify WIP PR creation and starting comment
|
# Verify WIP PR creation and starting comment
|
||||||
mock_client.create_pull_request.assert_called_once_with(
|
mock_client.prs.create_pull_request.assert_called_once_with(
|
||||||
"meeks", "repo1", head="fix/issue-42-add-x", base="master", title="WIP: add X", description="Work in progress for issue #42."
|
"meeks", "repo1", head="fix/issue-42-add-x", base="master", title="WIP: add X", description="Work in progress for issue #42."
|
||||||
)
|
)
|
||||||
mock_client.add_comment.assert_any_call("meeks", "repo1", 42, "Started work on PR #105 (http://gitea/pr/105).")
|
mock_client.issues.add_comment.assert_any_call("meeks", "repo1", 42, "Started work on PR #105 (http://gitea/pr/105).")
|
||||||
|
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
@patch("core.dispatcher.CodingAgent")
|
@patch("core.dispatcher.CodingAgent")
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_class: MagicMock, mock_run: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
# Existing WIP PR addressing issue #42
|
# Existing WIP PR addressing issue #42
|
||||||
wip_pr = PullRequestModel(
|
wip_pr = PullRequestModel(
|
||||||
@@ -430,14 +508,14 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
|
|||||||
state="open",
|
state="open",
|
||||||
head={"ref": "fix/issue-42-add-x"}
|
head={"ref": "fix/issue-42-add-x"}
|
||||||
)
|
)
|
||||||
mock_client.list_repo_pull_requests.return_value = [wip_pr]
|
mock_client.prs.list_repo_pull_requests.return_value = [wip_pr]
|
||||||
mock_client.get_pr_reviews.return_value = []
|
mock_client.prs.get_pr_reviews.return_value = []
|
||||||
|
|
||||||
mock_client.get_issue_comments.return_value = [
|
mock_client.issues.get_issue_comments.return_value = [
|
||||||
_make_comment("unknown-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
_make_comment("unknown-ai", "### Proposed Plan\n<!-- agent:plan-proposal -->\n<!-- agent:awaiting-reply -->"),
|
||||||
_make_comment("michael", "looks good, go ahead")
|
_make_comment("michael", "looks good, go ahead")
|
||||||
]
|
]
|
||||||
mock_client.get_pull_request_comments.return_value = []
|
mock_client.prs.get_pull_request_comments.return_value = []
|
||||||
|
|
||||||
# Mock planning agent deciding EXECUTE_PLAN
|
# Mock planning agent deciding EXECUTE_PLAN
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
@@ -448,7 +526,13 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
|
|||||||
# Mock coding agent executing plan
|
# Mock coding agent executing plan
|
||||||
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
|
mock_coding_class.return_value.run_with_tools = AsyncMock(return_value="PR Updated Successfully.")
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -462,14 +546,15 @@ async def test_dispatch_resumes_wip_pr(mock_coord_class: MagicMock, mock_coding_
|
|||||||
assert results[0] == "PR Updated Successfully."
|
assert results[0] == "PR Updated Successfully."
|
||||||
|
|
||||||
# Ensure create_pull_request was NOT called since it already exists
|
# Ensure create_pull_request was NOT called since it already exists
|
||||||
mock_client.create_pull_request.assert_not_called()
|
mock_client.prs.create_pull_request.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None:
|
async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
# PR authored by michael, requested reviewers is empty (agent not requested)
|
# PR authored by michael, requested reviewers is empty (agent not requested)
|
||||||
pr_detail = PullRequestModel(
|
pr_detail = PullRequestModel(
|
||||||
@@ -479,9 +564,15 @@ async def test_dispatch_skips_pr_if_not_requested_reviewer() -> None:
|
|||||||
user=UserModel(login="michael"),
|
user=UserModel(login="michael"),
|
||||||
requested_reviewers=[]
|
requested_reviewers=[]
|
||||||
)
|
)
|
||||||
mock_client.get_pull_request.return_value = pr_detail
|
mock_client.prs.get_pull_request.return_value = pr_detail
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="pr",
|
task_type="pr",
|
||||||
@@ -513,12 +604,13 @@ def test_coordinator_tools_registration() -> None:
|
|||||||
|
|
||||||
@patch("core.dispatcher.CoordinatorAgent")
|
@patch("core.dispatcher.CoordinatorAgent")
|
||||||
async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMock) -> None:
|
async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMock) -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
# Mock agent invoking propose_plan tool
|
# Mock agent invoking propose_plan tool
|
||||||
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
async def mock_decide(mission: str, planning_tools: list, coord_tools) -> str:
|
||||||
@@ -529,7 +621,13 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
|
|||||||
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
mock_coord_instance.decide_action = AsyncMock(side_effect=mock_decide)
|
||||||
mock_coord_class.return_value = mock_coord_instance
|
mock_coord_class.return_value = mock_coord_instance
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -541,7 +639,7 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
|
|||||||
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
results = await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
assert "POSTED_COMMENT: PROPOSE_PLAN" in results[0]
|
||||||
mock_client.add_comment.assert_called_once_with(
|
mock_client.issues.add_comment.assert_called_once_with(
|
||||||
"meeks",
|
"meeks",
|
||||||
"repo1",
|
"repo1",
|
||||||
42,
|
42,
|
||||||
@@ -550,15 +648,22 @@ async def test_dispatch_uses_coordinator_tool_calling(mock_coord_class: MagicMoc
|
|||||||
|
|
||||||
|
|
||||||
async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None:
|
async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
# Mock return values for methods called prior to the isinstance check
|
# Mock return values for methods called prior to the isinstance check
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
mock_client.get_issue_comments.return_value = []
|
mock_client.issues.get_issue_comments.return_value = []
|
||||||
mock_client.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
mock_client.repos.get_authenticated_user.return_value = UserModel(login="unknown-ai")
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Test dispatch raises TypeError if task_info is not IssueModel for an issue task
|
# 1. Test dispatch raises TypeError if task_info is not IssueModel for an issue task
|
||||||
work_item_invalid_issue = WorkItem(
|
work_item_invalid_issue = WorkItem(
|
||||||
@@ -588,13 +693,20 @@ async def test_dispatcher_raises_type_error_for_invalid_task_info() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def test_dispatch_fails_if_no_authenticated_user() -> None:
|
async def test_dispatch_fails_if_no_authenticated_user() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock()
|
||||||
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
|
mock_client.repos = MagicMock()
|
||||||
|
mock_tools: MagicMock = MagicMock()
|
||||||
|
|
||||||
# Simulate get_authenticated_user returning None
|
# Simulate get_authenticated_user returning None
|
||||||
mock_client.get_authenticated_user.return_value = None
|
mock_client.repos.get_authenticated_user.return_value = None
|
||||||
|
|
||||||
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
|
dispatcher = AgentDispatcher(
|
||||||
|
client=mock_client,
|
||||||
|
issue_tools=MagicMock(),
|
||||||
|
pr_tools=MagicMock(),
|
||||||
|
file_tools=MagicMock(),
|
||||||
|
git_tools=MagicMock(),
|
||||||
|
)
|
||||||
work_item = WorkItem(
|
work_item = WorkItem(
|
||||||
repo_full_name="meeks/repo1",
|
repo_full_name="meeks/repo1",
|
||||||
task_type="issue",
|
task_type="issue",
|
||||||
@@ -607,7 +719,7 @@ async def test_dispatch_fails_if_no_authenticated_user() -> None:
|
|||||||
await dispatcher.dispatch("meeks/repo1", [work_item])
|
await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
|
|
||||||
# Simulate get_authenticated_user raising an Exception
|
# Simulate get_authenticated_user raising an Exception
|
||||||
mock_client.get_authenticated_user.side_effect = Exception("API error")
|
mock_client.repos.get_authenticated_user.side_effect = Exception("API error")
|
||||||
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
||||||
await dispatcher.dispatch("meeks/repo1", [work_item])
|
await dispatcher.dispatch("meeks/repo1", [work_item])
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, AsyncMock, patch
|
|||||||
|
|
||||||
from core.orchestrator import AgentOrchestrator
|
from core.orchestrator import AgentOrchestrator
|
||||||
from gitea.client import GiteaClient
|
from gitea.client import GiteaClient
|
||||||
from gitea.tools.gitea_tools import GiteaTools
|
|
||||||
from gitea.models import IssueModel, PullRequestModel, RepositoryModel
|
from gitea.models import IssueModel, PullRequestModel, RepositoryModel
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
pytestmark = pytest.mark.anyio
|
||||||
@@ -31,7 +30,7 @@ async def test_poll_and_dispatch_no_notifications(
|
|||||||
) -> None:
|
) -> None:
|
||||||
mock_get_path.return_value = temp_state_file
|
mock_get_path.return_value = temp_state_file
|
||||||
mock_client = MagicMock(spec=GiteaClient)
|
mock_client = MagicMock(spec=GiteaClient)
|
||||||
mock_tools = MagicMock(spec=GiteaTools)
|
mock_tools = MagicMock()
|
||||||
|
|
||||||
# Return no notifications
|
# Return no notifications
|
||||||
mock_client.list_unread_notifications.return_value = []
|
mock_client.list_unread_notifications.return_value = []
|
||||||
@@ -68,7 +67,7 @@ async def test_poll_and_dispatch_with_notifications(
|
|||||||
mock_reader.decide_notification = AsyncMock(side_effect=mock_decide_notification)
|
mock_reader.decide_notification = AsyncMock(side_effect=mock_decide_notification)
|
||||||
mock_notification_reader_class.return_value = mock_reader
|
mock_notification_reader_class.return_value = mock_reader
|
||||||
mock_client = MagicMock(spec=GiteaClient)
|
mock_client = MagicMock(spec=GiteaClient)
|
||||||
mock_tools = MagicMock(spec=GiteaTools)
|
mock_tools = MagicMock()
|
||||||
|
|
||||||
# Set up mock Gitea notifications
|
# Set up mock Gitea notifications
|
||||||
notifications = [
|
notifications = [
|
||||||
|
|||||||
Reference in New Issue
Block a user