"""Tests for scripts/render_pr_comment.py (GitHub Action comment renderer).""" from __future__ import annotations import importlib.util import json import subprocess import sys from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] SCRIPT = REPO_ROOT / "scripts" / "render_pr_comment.py" FIXTURE = REPO_ROOT / "tests" / "fixtures" / "detect_changes_sample.json" _spec = importlib.util.spec_from_file_location("render_pr_comment", SCRIPT) assert _spec is not None and _spec.loader is not None render = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(render) @pytest.fixture() def report() -> dict: return json.loads(FIXTURE.read_text(encoding="utf-8")) # --------------------------------------------------------------------------- # risk_level # --------------------------------------------------------------------------- def test_risk_level_mapping(): assert render.risk_level(0.9) == "critical" assert render.risk_level(0.85) == "critical" assert render.risk_level(0.72) == "high" assert render.risk_level(0.7) == "high" assert render.risk_level(0.5) == "medium" assert render.risk_level(0.4) == "medium" assert render.risk_level(0.1) == "low" assert render.risk_level(0.0) == "low" # --------------------------------------------------------------------------- # md_escape # --------------------------------------------------------------------------- def test_md_escape_escapes_pipes_and_backticks(): escaped = render.md_escape("a|b`c") assert "|" not in escaped.replace("\\|", "") assert "\\|" in escaped assert "\\`" in escaped def test_md_escape_strips_control_chars_and_newlines(): escaped = render.md_escape("evil\x00name\nwith\rbreaks\x1b[31m") assert "\x00" not in escaped assert "\n" not in escaped assert "\r" not in escaped assert "\x1b" not in escaped def test_md_escape_caps_length(): escaped = render.md_escape("x" * 500) assert len(escaped) <= render._MAX_CELL # --------------------------------------------------------------------------- # render_markdown # --------------------------------------------------------------------------- def test_marker_is_first_line(report): body = render.render_markdown(report) assert body.splitlines()[0] == render.MARKER def test_overall_risk_line(report): body = render.render_markdown(report) assert "**Overall risk: 0.72 (HIGH)**" in body assert "3 changed function(s)/class(es)" in body assert "2 affected flow(s)" in body assert "1 test gap(s)" in body def test_risk_table_lists_top_functions(report): body = render.render_markdown(report) assert "### Risk-scored changes" in body assert render.md_escape("auth/session.py::rotate_token") in body assert render.md_escape("auth/session.py::validate_session") in body assert "| 0.72 | high |" in body assert "| 0.41 | medium |" in body assert "| 0.10 | low |" in body # Untested function marked "no", tested ones "yes". rotate_row = next(line for line in body.splitlines() if "rotate" in line and "| 0.72" in line) assert rotate_row.rstrip().endswith("| no |") validate_row = next(line for line in body.splitlines() if "| 0.41" in line) assert validate_row.rstrip().endswith("| yes |") def test_risk_table_location_includes_line_number(report): body = render.render_markdown(report) assert "auth/session.py:42" in body def test_affected_flows_section(report): body = render.render_markdown(report) assert "### Affected execution flows" in body assert render.md_escape("login_handler -> rotate_token") in body assert "criticality 0.83" in body assert "6 node(s) across 3 file(s)" in body def test_test_gaps_section(report): body = render.render_markdown(report) assert "### Test gaps" in body assert "(auth/session.py:42)" in body def test_token_savings_line(report): body = render.render_markdown(report) assert "**Token savings:**" in body assert "12,159" in body assert "94%" in body assert "estimated" in body def test_token_savings_line_omitted_when_zero(report): report["context_savings"] = {"estimated": True, "saved_tokens": 0, "saved_percent": 0} body = render.render_markdown(report) assert "**Token savings:**" not in body def test_token_savings_line_omitted_when_absent(report): del report["context_savings"] body = render.render_markdown(report) assert "**Token savings:**" not in body def test_footer_powered_by(report): body = render.render_markdown(report) assert "Powered by [code-review-graph]" in body assert "local-first" in body def test_max_functions_cap(report): body = render.render_markdown(report, max_functions=1) assert render.md_escape("auth/session.py::rotate_token") in body assert render.md_escape("auth/display.py::format_expiry") not in body assert "and 2 more changed symbol(s)" in body def test_max_flows_cap(report): body = render.render_markdown(report, max_flows=1) assert render.md_escape("login_handler -> rotate_token") in body assert render.md_escape("cli_main -> validate_session") not in body assert "and 1 more affected flow(s)" in body def test_truncated_analysis_note(report): report["functions_truncated"] = True body = render.render_markdown(report) assert "CRG_MAX_CHANGED_FUNCS" in body def test_markdown_injection_in_names_is_escaped(report): report["review_priorities"][0]["qualified_name"] = "x|y`z