diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 3e8576753f..414edd6553 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -125,6 +125,16 @@ def unique_strings(input_list: List[str]) -> List[str]: return unique_list +def _expand_minute_suffix(text: str) -> str: + """Replace minute abbreviations like '30m' with '30 minutes'. + + Only replaces when 'm' appears at a word boundary after digits + (e.g. "30m" -> "30 minutes"), leaving partial-unit strings like + "30ms" or "30min" unchanged. + """ + return re.sub(r'(\d+)m\b', r'\1 minutes', text) + + def convert_to_markdown_v2(output_data: dict, gfm_supported: bool = True, incremental_review=None, @@ -214,11 +224,17 @@ def convert_to_markdown_v2(output_data: dict, elif 'contribution time cost estimate' in key_nice.lower(): if gfm_supported: markdown_text += f"{emoji} Contribution time estimate (best, average, worst case): " - markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}" + best = _expand_minute_suffix(value['best_case']) + avg = _expand_minute_suffix(value['average_case']) + worst = _expand_minute_suffix(value['worst_case']) + markdown_text += f"{best} | {avg} | {worst}" markdown_text += f"\n" else: markdown_text += f"### {emoji} Contribution time estimate (best, average, worst case): " - markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}\n\n" + best = _expand_minute_suffix(value['best_case']) + avg = _expand_minute_suffix(value['average_case']) + worst = _expand_minute_suffix(value['worst_case']) + markdown_text += f"{best} | {avg} | {worst}\n\n" elif 'security concerns' in key_nice.lower(): if gfm_supported: markdown_text += f"" diff --git a/tests/unittest/test_convert_to_markdown.py b/tests/unittest/test_convert_to_markdown.py index 0d18e03cec..980d735a58 100644 --- a/tests/unittest/test_convert_to_markdown.py +++ b/tests/unittest/test_convert_to_markdown.py @@ -2,7 +2,7 @@ import textwrap from unittest.mock import Mock -from pr_agent.algo.utils import PRReviewHeader, convert_to_markdown_v2 +from pr_agent.algo.utils import PRReviewHeader, _expand_minute_suffix, convert_to_markdown_v2 from pr_agent.tools.pr_description import insert_br_after_x_chars """ @@ -303,3 +303,23 @@ def test_br3(self): ' and implements
aaa') # print("-----") # print(file_change_description_br) + + +class TestExpandMinuteSuffix: + """Tests for _expand_minute_suffix regex replacement.""" + + def test_standalone_minute_suffix(self): + """'30m' at end of string becomes '30 minutes'.""" + assert _expand_minute_suffix("30m") == "30 minutes" + + def test_minute_suffix_not_replaced_when_part_of_longer_unit(self): + """'30ms' stays unchanged because 'm' is not at a word boundary.""" + assert _expand_minute_suffix("30ms") == "30ms" + + def test_minute_suffix_replaced_before_space(self): + """'30m implementation' replaces '30m' because 'm' is at a word boundary.""" + assert _expand_minute_suffix("30m implementation") == "30 minutes implementation" + + def test_minute_suffix_in_compound_estimate(self): + """'2h 30m' becomes '2h 30 minutes' (only the minute part is replaced).""" + assert _expand_minute_suffix("2h 30m") == "2h 30 minutes"