diff --git a/docs/docs/tools/improve.md b/docs/docs/tools/improve.md
index afbb9adcf4..c46ca737ef 100644
--- a/docs/docs/tools/improve.md
+++ b/docs/docs/tools/improve.md
@@ -60,9 +60,9 @@ num_code_suggestions_per_chunk = ...
### Table vs Committable code comments
-PR-Agent supports two modes for presenting code suggestions:
+PR-Agent supports two modes for presenting code suggestions:
-1) [Table](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png) mode
+1) [Table](https://codium.ai/images/pr_agent/code_suggestions_as_comment_closed.png) mode
2) [Inline Committable](https://codium.ai/images/pr_agent/improve.png) code comments mode.
@@ -74,7 +74,7 @@ The table format offers several key advantages:
- **Centralized tracking**: Shows suggestion implementation status in one place
- **IDE integration**: Allows applying suggestions directly in your IDE via the CLI tool
-Table mode is the default of PR-Agent, and is recommended approach for most users due to these benefits.
+Table mode is the default of PR-Agent, and is recommended approach for most users due to these benefits.
{width=512}
@@ -113,9 +113,9 @@ Use triple quotes to write multi-line instructions. Use bullet points or numbers
PR-Agent supports both simple and hierarchical best practices configurations to provide guidance to the AI model for generating relevant code suggestions.
???- tip "Writing effective best practices files"
-
+
The following guidelines apply to all best practices files:
-
+
- Write clearly and concisely
- Include brief code examples when helpful with before/after patterns
- Focus on project-specific guidelines that will result in relevant suggestions you actually want to get
@@ -126,9 +126,9 @@ PR-Agent supports both simple and hierarchical best practices configurations to
- Use pattern-based structure rather than simple bullet points for better clarity
???- tip "Example of a best practices file"
-
+
Pattern 1: Add proper error handling with try-except blocks around external function calls.
-
+
Example code before:
```python
@@ -147,7 +147,7 @@ PR-Agent supports both simple and hierarchical best practices configurations to
```
Pattern 2: Add defensive null/empty checks before accessing object properties or performing operations on potentially null variables to prevent runtime errors.
-
+
Example code before:
```python
@@ -315,7 +315,7 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line
| focus_only_on_problems |
- If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is true. |
+ If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is true. |
| persistent_comment |
@@ -337,6 +337,13 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line
publish_output_no_suggestions |
If set to true, the tool will publish a comment even if no suggestions were found. Default is true. |
+
+ | enable_suggestions_coverage_footer |
+
+ If set to true, the tool will display a coverage notice when failed analysis chunks make the
+ suggestions incomplete. Default is true.
+ |
+
???+ example "Params for number of suggestions and AI calls"
diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py
index 5a321620d1..3b3d2b868f 100644
--- a/pr_agent/git_providers/git_provider.py
+++ b/pr_agent/git_providers/git_provider.py
@@ -1,13 +1,14 @@
-from abc import ABC, abstractmethod
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
import os
import shutil
import subprocess
import time
+from abc import ABC, abstractmethod
from typing import Optional, Tuple
from pr_agent.algo.types import FilePatchInfo
-from pr_agent.algo.utils import Range, add_pr_review_identity, comment_matches_identity, process_description
+from pr_agent.algo.utils import (Range, add_pr_review_identity,
+ comment_matches_identity, process_description)
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
@@ -130,6 +131,17 @@ def supports_code_suggestions_artifact(self) -> bool:
"""Return whether `publish_code_suggestions()` writes a standalone output artifact."""
return False
+ def publish_code_suggestions_artifact(
+ self, code_suggestions: list, artifact_footer: str = "",
+ no_suggestions_message: str = "No code suggestions found for the PR.") -> bool:
+ """Publish suggestions to a standalone artifact, optionally with additional context.
+
+ Providers that return True from `supports_code_suggestions_artifact()` should override
+ this method when they can preserve the footer in the same artifact. The default keeps
+ backward compatibility for providers that only implement `publish_code_suggestions()`.
+ """
+ return self.publish_code_suggestions(code_suggestions)
+
#Given a url (issues or PR/MR) - get the .git repo url to which they belong. Needs to be implemented by the provider.
def get_git_repo_url(self, issues_or_pr_url: str) -> str:
get_logger().warning("Not implemented! Returning empty url")
diff --git a/pr_agent/git_providers/local_git_provider.py b/pr_agent/git_providers/local_git_provider.py
index 310b3e19e6..5795c8b5fb 100644
--- a/pr_agent/git_providers/local_git_provider.py
+++ b/pr_agent/git_providers/local_git_provider.py
@@ -145,6 +145,11 @@ def publish_code_suggestion(self, body: str, relevant_file: str,
raise NotImplementedError('Publishing code suggestions is not implemented for the local git provider')
def publish_code_suggestions(self, code_suggestions: list) -> bool:
+ return self.publish_code_suggestions_artifact(code_suggestions)
+
+ def publish_code_suggestions_artifact(
+ self, code_suggestions: list, artifact_footer: str = "",
+ no_suggestions_message: str = "No code suggestions found for the PR.") -> bool:
"""
Write /improve output to a file (improve.md by default).
@@ -167,7 +172,8 @@ def publish_code_suggestions(self, code_suggestions: list) -> bool:
sections.append(f"{header}\n\n{suggestion.get('body', '').strip()}")
header = format_pr_code_suggestions_header(markdown_level=1)
pr_body = f"{header}\n\n" + "\n\n".join(sections) if sections \
- else f"{header}\n\nNo code suggestions found for the PR."
+ else f"{header}\n\n{no_suggestions_message}"
+ pr_body += artifact_footer
if not sections and get_settings().get("config.output_run_details", False):
pr_body += show_run_details(False)
with open(self.improve_path, "w", encoding="utf-8") as file:
diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml
index 4d133183e9..e454f196b2 100644
--- a/pr_agent/settings/configuration.toml
+++ b/pr_agent/settings/configuration.toml
@@ -179,6 +179,7 @@ enable_chat_text=false
persistent_comment=true
max_history_len=4
publish_output_no_suggestions=true
+enable_suggestions_coverage_footer=true # show when failed analysis chunks make the suggestions incomplete
# suggestions scoring
suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions
new_score_mechanism=true
diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py
index a45dd9df18..f253eda818 100644
--- a/pr_agent/tools/pr_code_suggestions.py
+++ b/pr_agent/tools/pr_code_suggestions.py
@@ -30,7 +30,8 @@
format_pr_code_suggestions_header,
get_max_tokens, get_model, load_yaml,
replace_code_tags,
- show_relevant_configurations, show_run_details)
+ show_relevant_configurations,
+ show_run_details)
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import (AzureDevopsProvider, GithubProvider,
GitLabProvider, get_git_provider,
@@ -207,6 +208,7 @@ async def run(self):
# generate summarized suggestions
pr_body = self.generate_summarized_suggestions(data)
+ pr_body += self._get_suggestions_coverage_footer()
get_logger().debug(f"PR output", artifact=pr_body)
# require self-review
@@ -266,6 +268,7 @@ async def run(self):
else:
get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.')
pr_body = self.generate_summarized_suggestions(data)
+ pr_body += self._get_suggestions_coverage_footer()
get_settings().data = {"artifact": pr_body}
return
except Exception as e:
@@ -296,13 +299,30 @@ async def add_self_review_text(self, pr_body):
pr_body += ' '
return pr_body
+ def _get_suggestions_coverage_footer(self, suggestions_present: bool = True) -> str:
+ failed_chunk_count = getattr(self, "failed_chunk_count", 0)
+ if (not failed_chunk_count or
+ not get_settings().pr_code_suggestions.get("enable_suggestions_coverage_footer", True)):
+ return ""
+ total_chunk_count = getattr(self, "total_chunk_count", failed_chunk_count)
+ coverage_detail = ("the suggestions above are based on the successful chunks only."
+ if suggestions_present else
+ "no suggestions were found in the successful chunks; failed chunks could not be analyzed.")
+ return (f"\n\n⚠️ **Suggestion coverage:** {failed_chunk_count} of {total_chunk_count} "
+ "analysis chunks failed; "
+ f"{coverage_detail}")
+
async def publish_no_suggestions(self):
- pr_body = f"{format_pr_code_suggestions_header()}\n\nNo code suggestions found for the PR."
+ coverage_footer = self._get_suggestions_coverage_footer(suggestions_present=False)
+ no_suggestions_message = ("No code suggestions found in the successfully analyzed chunks."
+ if coverage_footer else "No code suggestions found for the PR.")
+ pr_body = f"{format_pr_code_suggestions_header()}\n\n{no_suggestions_message}{coverage_footer}"
if (get_settings().config.publish_output and
get_settings().pr_code_suggestions.get('publish_output_no_suggestions', True)):
get_logger().warning("No code suggestions found for the PR.")
- if self.git_provider.supports_code_suggestions_artifact():
- self.git_provider.publish_code_suggestions([])
+ if self.git_provider.supports_code_suggestions_artifact() is True:
+ self.git_provider.publish_code_suggestions_artifact(
+ [], artifact_footer=coverage_footer, no_suggestions_message=no_suggestions_message)
return
pr_body = add_comment_identity(
pr_body,
@@ -318,7 +338,7 @@ async def publish_no_suggestions(self):
else:
self.git_provider.publish_comment(pr_body)
else:
- get_settings().data = {"artifact": ""}
+ get_settings().data = {"artifact": pr_body if coverage_footer else ""}
if self.progress_response:
self.git_provider.remove_comment(self.progress_response)
@@ -337,7 +357,7 @@ async def dual_publishing(self, data):
if data_above_threshold['code_suggestions']:
get_logger().info(
f"Publishing {len(data_above_threshold['code_suggestions'])} suggestions in dual publishing mode")
- await self.push_inline_code_suggestions(data_above_threshold)
+ await self.push_inline_code_suggestions(data_above_threshold, include_coverage_footer=False)
except Exception as e:
get_logger().error(f"Failed to publish dual publishing suggestions, error: {e}")
@@ -740,17 +760,24 @@ def _prepare_pr_code_suggestions(self, predictions: str) -> Dict:
return data
- async def push_inline_code_suggestions(self, data):
+ async def push_inline_code_suggestions(self, data, include_coverage_footer: bool = True):
code_suggestions = []
fallback_comments = []
+ coverage_footer = self._get_suggestions_coverage_footer() if include_coverage_footer else ""
+ supports_suggestions_artifact = self.git_provider.supports_code_suggestions_artifact() is True
if not data['code_suggestions']:
get_logger().info('No suggestions found to improve this PR.')
+ empty_coverage_footer = (self._get_suggestions_coverage_footer(suggestions_present=False)
+ if include_coverage_footer else "")
+ no_suggestions_message = ("No suggestions found in the successfully analyzed chunks."
+ if empty_coverage_footer else "No suggestions found to improve this PR.")
+ pr_body = no_suggestions_message + empty_coverage_footer
if self.progress_response:
return self.git_provider.edit_comment(self.progress_response,
- body='No suggestions found to improve this PR.')
+ body=pr_body)
else:
- return self.git_provider.publish_comment('No suggestions found to improve this PR.')
+ return self.git_provider.publish_comment(pr_body)
for d in data['code_suggestions']:
try:
@@ -796,11 +823,17 @@ async def push_inline_code_suggestions(self, data):
'original_suggestion': d})
if code_suggestions:
- is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
+ if supports_suggestions_artifact:
+ is_successful = self.git_provider.publish_code_suggestions_artifact(
+ code_suggestions, artifact_footer=coverage_footer)
+ else:
+ is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
if not is_successful:
get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately")
for code_suggestion in code_suggestions:
self.git_provider.publish_code_suggestions([code_suggestion])
+ if coverage_footer and not supports_suggestions_artifact:
+ fallback_comments.append(coverage_footer.strip())
if fallback_comments:
self.git_provider.publish_comment("\n\n---\n\n".join(fallback_comments))
@@ -1112,6 +1145,8 @@ def remove_line_numbers(self, patches_diff_list: List[str]) -> List[str]:
return patches_diff_list
async def prepare_prediction_main(self, model: str) -> dict:
+ self.failed_chunk_count = 0
+ self.total_chunk_count = 0
# get PR diff
if get_settings().pr_code_suggestions.decouple_hunks:
self.patches_diff_list = get_pr_multi_diffs(self.git_provider,
@@ -1145,6 +1180,7 @@ async def prepare_prediction_main(self, model: str) -> dict:
prediction_list = []
chunk_errors = []
chunk_pairs = list(zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers))
+ self.total_chunk_count = len(chunk_pairs)
# parallelize calls to AI:
if get_settings().pr_code_suggestions.parallel_calls:
@@ -1179,6 +1215,7 @@ async def prepare_prediction_main(self, model: str) -> dict:
else:
prediction_list.append(prediction)
+ self.failed_chunk_count = len(chunk_errors)
if chunk_errors and not prediction_list:
raise chunk_errors[0]
self.prediction_list = prediction_list
diff --git a/tests/unittest/test_local_git_provider.py b/tests/unittest/test_local_git_provider.py
index 1c423e4b6c..1e05efeafb 100644
--- a/tests/unittest/test_local_git_provider.py
+++ b/tests/unittest/test_local_git_provider.py
@@ -121,6 +121,22 @@ def test_publish_code_suggestions_no_suggestions(tmp_path):
assert "No code suggestions found" in improve_path.read_text()
+def test_publish_code_suggestions_artifact_includes_partial_coverage(tmp_path):
+ improve_path = tmp_path / "improve.md"
+ provider = object.__new__(LocalGitProvider)
+ provider.improve_path = improve_path
+
+ assert provider.publish_code_suggestions_artifact(
+ [],
+ artifact_footer="\n\n⚠️ **Suggestion coverage:** 1 of 2 analysis chunks failed.",
+ no_suggestions_message="No code suggestions found in the successfully analyzed chunks.",
+ ) is True
+
+ content = improve_path.read_text()
+ assert "No code suggestions found in the successfully analyzed chunks." in content
+ assert "1 of 2 analysis chunks failed" in content
+
+
def test_publish_code_suggestions_uses_custom_heading_without_identity(tmp_path):
snapshot = snapshot_settings(["pr_code_suggestions.suggestions_heading"])
improve_path = tmp_path / "improve.md"
diff --git a/tests/unittest/test_pr_code_suggestions_core.py b/tests/unittest/test_pr_code_suggestions_core.py
index 61a0766164..05e6ad26d0 100644
--- a/tests/unittest/test_pr_code_suggestions_core.py
+++ b/tests/unittest/test_pr_code_suggestions_core.py
@@ -11,7 +11,8 @@
from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import GitProvider, IncrementalPR
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
-from tests.unittest._settings_helpers import restore_settings, snapshot_settings
+from tests.unittest._settings_helpers import (restore_settings,
+ snapshot_settings)
def _make_tool(git_provider=None):
@@ -156,6 +157,8 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers)
assert calls == ["chunk-a", "chunk-b"]
assert successful_chunk_finished.is_set()
+ assert tool.failed_chunk_count == 1
+ assert tool.total_chunk_count == 2
assert data["code_suggestions"] == [_valid_suggestion(relevant_file="chunk-a.py")]
@@ -219,6 +222,8 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers)
settings.pr_code_suggestions.parallel_calls = original_parallel_calls
assert calls == ["chunk-a", "chunk-b", "chunk-c"]
+ assert tool.failed_chunk_count == 1
+ assert tool.total_chunk_count == 3
assert data["code_suggestions"] == [
_valid_suggestion(relevant_file="chunk-a.py"),
_valid_suggestion(relevant_file="chunk-c.py"),
@@ -266,6 +271,66 @@ async def fake_get_prediction(model, patches_diff, patches_diff_no_line_numbers)
("fallback-model", "chunk-b"),
]
assert len(data["code_suggestions"]) == 2
+ assert tool.failed_chunk_count == 0
+ assert tool.total_chunk_count == 2
+
+
+def test_suggestions_coverage_footer_reports_partial_runs_and_respects_flag():
+ settings = get_settings()
+ snapshot = snapshot_settings(["pr_code_suggestions.enable_suggestions_coverage_footer"])
+ tool = _make_tool()
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 3
+
+ try:
+ settings.set("pr_code_suggestions.enable_suggestions_coverage_footer", True)
+ footer = tool._get_suggestions_coverage_footer()
+ assert "1 of 3 analysis chunks failed" in footer
+ assert "successful chunks only" in footer
+
+ empty_footer = tool._get_suggestions_coverage_footer(suggestions_present=False)
+ assert "no suggestions were found in the successful chunks" in empty_footer
+ assert "failed chunks could not be analyzed" in empty_footer
+
+ settings.set("pr_code_suggestions.enable_suggestions_coverage_footer", False)
+ assert tool._get_suggestions_coverage_footer() == ""
+ finally:
+ restore_settings(snapshot)
+
+
+def test_suggestions_coverage_footer_is_safe_for_tools_built_without_init():
+ tool = _make_tool()
+
+ assert tool._get_suggestions_coverage_footer() == ""
+
+
+@pytest.mark.asyncio
+async def test_run_appends_partial_suggestions_coverage_to_the_summary():
+ snapshot = snapshot_settings([
+ "config.publish_output",
+ "data",
+ "pr_code_suggestions.enable_suggestions_coverage_footer",
+ ])
+ tool = _make_tool()
+ tool.pr_url = "https://example.test/pull/1"
+ tool.git_provider.get_files.return_value = ["app.py"]
+ tool.generate_summarized_suggestions = MagicMock(return_value="Base suggestions body")
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 2
+
+ try:
+ get_settings().set("config.publish_output", False)
+ get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True)
+ with (patch("pr_agent.tools.pr_code_suggestions.init_run_details"),
+ patch("pr_agent.tools.pr_code_suggestions.retry_with_fallback_models",
+ AsyncMock(return_value={"code_suggestions": [_valid_suggestion()]}))):
+ await tool.run()
+
+ artifact = get_settings().data["artifact"]
+ assert artifact.startswith("Base suggestions body")
+ assert "1 of 2 analysis chunks failed" in artifact
+ finally:
+ restore_settings(snapshot)
def test_dedent_code_matches_target_file_indentation():
@@ -747,6 +812,8 @@ def test_summarized_suggestions_normalize_both_sides_of_the_diff():
async def test_suggestion_covering_the_anchored_range_is_published_as_committable():
git_provider = _provider_with_file("def f():\n return old()\n")
tool = _make_tool(git_provider)
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 2
await tool.push_inline_code_suggestions({"code_suggestions": [
_valid_suggestion(
@@ -889,6 +956,28 @@ async def test_publish_no_suggestions_still_overwrites_the_progress_comment_when
git_provider.remove_comment.assert_not_called()
+@pytest.mark.asyncio
+async def test_publish_no_suggestions_qualifies_partial_results(publish_output_no_suggestions):
+ publish_output_no_suggestions(True)
+ snapshot = snapshot_settings(["pr_code_suggestions.enable_suggestions_coverage_footer"])
+ git_provider = MagicMock()
+ git_provider.supports_code_suggestions_artifact.return_value = False
+ tool = _make_tool(git_provider)
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 2
+
+ try:
+ get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True)
+ await tool.publish_no_suggestions()
+ finally:
+ restore_settings(snapshot)
+
+ body = git_provider.publish_comment.call_args.args[0]
+ assert "No code suggestions found in the successfully analyzed chunks." in body
+ assert "1 of 2 analysis chunks failed" in body
+ assert "failed chunks could not be analyzed" in body
+
+
@pytest.mark.asyncio
async def test_publish_no_suggestions_uses_provider_artifact_capability(publish_output_no_suggestions):
publish_output_no_suggestions(True)
@@ -898,11 +987,38 @@ async def test_publish_no_suggestions_uses_provider_artifact_capability(publish_
await tool.publish_no_suggestions()
- git_provider.publish_code_suggestions.assert_called_once_with([])
+ git_provider.publish_code_suggestions_artifact.assert_called_once_with(
+ [], artifact_footer="", no_suggestions_message="No code suggestions found for the PR.")
+ git_provider.publish_code_suggestions.assert_not_called()
git_provider.publish_comment.assert_not_called()
git_provider.edit_comment.assert_not_called()
+@pytest.mark.asyncio
+async def test_publish_no_suggestions_keeps_partial_notice_in_disabled_output_artifact(
+ publish_output_no_suggestions):
+ publish_output_no_suggestions(True)
+ snapshot = snapshot_settings([
+ "config.publish_output",
+ "data",
+ "pr_code_suggestions.enable_suggestions_coverage_footer",
+ ])
+ tool = _make_tool()
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 2
+
+ try:
+ get_settings().set("config.publish_output", False)
+ get_settings().set("pr_code_suggestions.enable_suggestions_coverage_footer", True)
+ await tool.publish_no_suggestions()
+ artifact = get_settings().data["artifact"]
+ finally:
+ restore_settings(snapshot)
+
+ assert "No code suggestions found in the successfully analyzed chunks." in artifact
+ assert "1 of 2 analysis chunks failed" in artifact
+
+
def test_setup_incremental_scope_calls_provider_when_supported():
git_provider = MagicMock()
git_provider.supports_incremental_kind.return_value = True
@@ -1025,6 +1141,7 @@ async def test_dual_publishing_keeps_suggestions_without_replacement_code():
]})
assert "Use the shared helper." in _published_suggestion(git_provider)["body"]
+ git_provider.publish_comment.assert_not_called()
finally:
settings.set("pr_code_suggestions.dual_publishing_score_threshold", original_threshold)
diff --git a/tests/unittest/test_pr_code_suggestions_rendering.py b/tests/unittest/test_pr_code_suggestions_rendering.py
index c3eeebc0f3..05978dad0b 100644
--- a/tests/unittest/test_pr_code_suggestions_rendering.py
+++ b/tests/unittest/test_pr_code_suggestions_rendering.py
@@ -5,7 +5,8 @@
from pr_agent.algo.types import FilePatchInfo
from pr_agent.config_loader import get_settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
-from tests.unittest._settings_helpers import restore_settings, snapshot_settings
+from tests.unittest._settings_helpers import (restore_settings,
+ snapshot_settings)
TRUNCATION_SETTINGS = (
"pr_code_suggestions.max_code_suggestion_length",
@@ -132,6 +133,31 @@ async def test_push_inline_renders_body_with_score_and_label():
assert args[0]["original_suggestion"]["one_sentence_summary"] == "Use the shared helper"
+@pytest.mark.asyncio
+async def test_push_inline_publishes_partial_coverage_notice():
+ git_provider = MagicMock()
+ git_provider.diff_files = [
+ FilePatchInfo(
+ base_file="",
+ head_file="def f():\n return old()\n",
+ patch="",
+ filename="app.py",
+ )
+ ]
+ git_provider.publish_code_suggestions.return_value = True
+ git_provider.supports_code_suggestions_artifact.return_value = False
+ tool = _make_tool(git_provider)
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 2
+
+ await tool.push_inline_code_suggestions({"code_suggestions": [_suggestion()]})
+
+ git_provider.publish_code_suggestions.assert_called_once()
+ coverage_comment = git_provider.publish_comment.call_args.args[0]
+ assert "1 of 2 analysis chunks failed" in coverage_comment
+ assert "successful chunks only" in coverage_comment
+
+
@pytest.mark.asyncio
async def test_push_inline_renders_body_without_score_when_missing_or_zero():
git_provider = MagicMock()
@@ -169,6 +195,21 @@ async def test_push_inline_publishes_no_suggestions_comment_when_empty():
git_provider.publish_code_suggestions.assert_not_called()
+@pytest.mark.asyncio
+async def test_push_inline_qualifies_empty_partial_results():
+ git_provider = MagicMock()
+ tool = _make_tool(git_provider)
+ tool.failed_chunk_count = 1
+ tool.total_chunk_count = 3
+
+ await tool.push_inline_code_suggestions({"code_suggestions": []})
+
+ body = git_provider.publish_comment.call_args.args[0]
+ assert "successfully analyzed chunks" in body
+ assert "1 of 3 analysis chunks failed" in body
+ assert "could not be analyzed" in body
+
+
# ---------------------------------------------------------------------------
# generate_summarized_suggestions
# ---------------------------------------------------------------------------