Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions pr_agent/tools/pr_description.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,11 +461,9 @@ def _prepare_data(self):
if 'description' in self.data:
self.data['description'] = self.data.pop('description')
if 'changes_diagram' in self.data:
changes_diagram = self.data.pop('changes_diagram').strip()
if changes_diagram.startswith('```'):
if not changes_diagram.endswith('```'): # fallback for missing closing
changes_diagram += '\n```'
self.data['changes_diagram'] = '\n'+ changes_diagram
sanitized = sanitize_diagram(self.data.pop('changes_diagram'))
if sanitized:
self.data['changes_diagram'] = sanitized
if 'pr_files' in self.data:
self.data['pr_files'] = self.data.pop('pr_files')

Expand Down Expand Up @@ -771,6 +769,30 @@ def add_file_data(self, delta_nbsp, diff_plus_minus, file_change_description_br,
"""
return pr_body

def sanitize_diagram(diagram_raw: str) -> str:
"""Sanitize a diagram string: fix missing closing fence and remove backticks."""
if not isinstance(diagram_raw, str):
return ''
diagram = diagram_raw.strip()
if not diagram.startswith('```mermaid'):
return ''

# fallback missing closing
if not diagram.endswith('```'):
diagram += '\n```'

# remove backticks inside node labels: ["`label`"] -> ["label"]
result = []
for line in diagram.split('\n'):
line = re.sub(
r'\["([^"]*?)"\]',
lambda m: '["' + m.group(1).replace('`', '') + '"]',
line,
)
result.append(line)
return '\n' + '\n'.join(result)


def count_chars_without_html(string):
if '<' not in string:
return len(string)
Expand Down
78 changes: 78 additions & 0 deletions tests/unittest/test_pr_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pytest
import yaml
from unittest.mock import MagicMock, patch
from pr_agent.tools.pr_description import PRDescription, sanitize_diagram

KEYS_FIX = ["filename:", "language:", "changes_summary:", "changes_title:", "description:", "title:"]

def _make_instance(prediction_yaml: str):
"""Create a PRDescription instance, bypassing __init__."""
with patch.object(PRDescription, '__init__', lambda self, *a, **kw: None):
obj = PRDescription.__new__(PRDescription)
obj.prediction = prediction_yaml
obj.keys_fix = KEYS_FIX
obj.user_description = ""
return obj


def _mock_settings():
"""Mock get_settings used by _prepare_data."""
settings = MagicMock()
settings.pr_description.add_original_user_description = False
return settings


def _prediction_with_diagram(diagram_value: str) -> str:
"""Build a minimal YAML prediction string that includes changes_diagram."""
return yaml.dump({
'title': 'test',
'description': 'test',
'changes_diagram': diagram_value,
})


class TestPRDescriptionDiagram:

@patch('pr_agent.tools.pr_description.get_settings')
def test_diagram_not_starting_with_fence_is_removed(self, mock_get_settings):
mock_get_settings.return_value = _mock_settings()
obj = _make_instance(_prediction_with_diagram('graph LR\nA --> B'))
obj._prepare_data()
assert 'changes_diagram' not in obj.data

@patch('pr_agent.tools.pr_description.get_settings')
def test_diagram_missing_closing_fence_is_appended(self, mock_get_settings):
mock_get_settings.return_value = _mock_settings()
obj = _make_instance(_prediction_with_diagram('```mermaid\ngraph LR\nA --> B'))
obj._prepare_data()
assert obj.data['changes_diagram'] == '\n```mermaid\ngraph LR\nA --> B\n```'

@patch('pr_agent.tools.pr_description.get_settings')
def test_backticks_inside_label_are_removed(self, mock_get_settings):
mock_get_settings.return_value = _mock_settings()
obj = _make_instance(_prediction_with_diagram('```mermaid\ngraph LR\nA["`file`"] --> B\n```'))
obj._prepare_data()
assert obj.data['changes_diagram'] == '\n```mermaid\ngraph LR\nA["file"] --> B\n```'

@patch('pr_agent.tools.pr_description.get_settings')
def test_backticks_outside_label_are_kept(self, mock_get_settings):
mock_get_settings.return_value = _mock_settings()
obj = _make_instance(_prediction_with_diagram('```mermaid\ngraph LR\nA["`file`"] -->|`edge`| B\n```'))
obj._prepare_data()
assert obj.data['changes_diagram'] == '\n```mermaid\ngraph LR\nA["file"] -->|`edge`| B\n```'

@patch('pr_agent.tools.pr_description.get_settings')
def test_normal_diagram_only_adds_newline(self, mock_get_settings):
mock_get_settings.return_value = _mock_settings()
obj = _make_instance(_prediction_with_diagram('```mermaid\ngraph LR\nA["file.py"] --> B["output"]\n```'))
obj._prepare_data()
assert obj.data['changes_diagram'] == '\n```mermaid\ngraph LR\nA["file.py"] --> B["output"]\n```'

def test_none_input_returns_empty(self):
assert sanitize_diagram(None) == ''

def test_non_string_input_returns_empty(self):
assert sanitize_diagram(123) == ''

def test_non_mermaid_fence_returns_empty(self):
assert sanitize_diagram('```python\nprint("hello")\n```') == ''