"""Tests for the unified-review HTML/Markdown report tool.""" from __future__ import annotations import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).parent.parent)) from code_review_graph.scoring import ( # noqa: E402 build_report_data, render_markdown_report, ) from code_review_graph.tools.scoring_tools import ( # noqa: E402 _load_report_template, generate_report_func, ) def _review_data() -> dict: return { "scope": "change-level", "tier": "standard", "timestamp": "2026-08-05T00:00:00Z", "files": "app.py, main.py", "baseline": "generic", "verdict": "\u2705 PASS", "quality_score": 7.0, "counts": {"critical": 1, "informational": 2}, "metrics": { "sql_risk": {"value": 0, "grade": "good", "note": "heuristic"}, "exception_coverage": {"value": 33.33, "grade": "warn", "note": "heuristic"}, }, "findings": [ { "severity": "MAJOR", "category": "Data", "message": "Possible N+1 query", "path": "src/app.py", "line": 42, "confidence": 7, "fix": "Add eager loading", }, ], "manual_review": ["Payment callback idempotency"], "llm_judged": ["requirement_coverage", "logic_alignment"], } def _mkroot(tmp_path: Path) -> Path: Path(tmp_path, ".code-review-graph").mkdir(exist_ok=True) return tmp_path class TestBuildReportData: def test_normalises_review_data(self): data = build_report_data(_review_data()) assert data["verdict"] == "\u2705 PASS" assert data["tier"] == "standard" assert data["scope"] == "change-level" assert data["metrics"]["sql_risk"]["grade"] == "good" assert len(data["issues"]) == 1 assert data["issues"][0]["location"] == "src/app.py:42" assert "requirement_coverage" in data["llm_judged"] def test_quality_score_and_counts_carried(self): data = build_report_data(_review_data()) assert data["quality_score"] == 7.0 assert data["counts"] == {"critical": 1, "informational": 2} def test_empty_findings(self): rd = _review_data() rd["findings"] = [] data = build_report_data(rd) assert data["issues"] == [] def test_defaults(self): data = build_report_data({}) assert data["scope"] == "change-level" assert data["tier"] == "standard" assert data["verdict"] == "\u274c FAIL" assert data["issues"] == [] def test_reviewed_files_carried(self): rd = _review_data() rd["reviewed_files"] = ["src/a.py", "src/b.py"] data = build_report_data(rd) assert data["reviewed_files"] == ["src/a.py", "src/b.py"] def test_reviewed_files_default_empty(self): data = build_report_data(_review_data()) assert data["reviewed_files"] == [] def test_files_list_normalised_to_string(self): rd = _review_data() rd["files"] = ["src/a.py", "src/b.py"] data = build_report_data(rd) assert data["files"] == "src/a.py, src/b.py" def test_reviewed_files_fallback_from_files_list(self): rd = _review_data() rd["files"] = ["src/a.py", "src/b.py"] data = build_report_data(rd) assert data["reviewed_files"] == ["src/a.py", "src/b.py"] # explicit reviewed_files wins over the files-list fallback rd["reviewed_files"] = ["src/a.py"] assert build_report_data(rd)["reviewed_files"] == ["src/a.py"] def test_reviewed_files_string_split(self): """A comma-separated STRING reviewed_files must be split into an array (agents pass both formats). Guards against iterating the string char-by-char (which rendered 's','e','r'... as fake files).""" rd = _review_data() rd["reviewed_files"] = "src/a.rs, src/b.rs, " data = build_report_data(rd) assert data["reviewed_files"] == ["src/a.rs", "src/b.rs"] md = render_markdown_report(rd) assert "src/a.rs" in md assert "src/b.rs" in md # no single-char file entries assert "- `s`" not in md assert "265" not in md def test_metrics_filters_non_objective_keys(self): rd = _review_data() rd["metrics"]["blast_radius"] = { "direct_changed_nodes": 101, "impacted_nodes": 13, } rd["metrics"]["objective_grade"] = "fail" data = build_report_data(rd) assert "blast_radius" not in data["metrics"] assert "objective_grade" not in data["metrics"] assert set(data["metrics"]) == { "sql_risk", "exception_coverage", } class TestLoadTemplate: def test_loads_package_asset(self): template = _load_report_template() assert "{{REPORT_DATA}}" in template assert template.startswith("") class TestRenderMarkdownReport: def test_renders_chinese_structure(self): md = render_markdown_report(_review_data()) assert "# 代码审查报告" in md assert "结论" in md assert "PR 质量分" in md and "7.0/10" in md assert "客观指标" in md assert "SQL 注入风险" in md assert "异常分支覆盖" in md assert "问题清单" in md assert "🟡 主要" in md assert "置信度 7/10" in md assert "修复建议" in md assert "需要人工审查" in md assert "Payment callback idempotency" in md assert "需 LLM 判断的指标" in md def test_grade_localisation(self): rd = _review_data() rd["metrics"] = { "sql_risk": {"value": 0, "grade": "good"}, "vulnerability_risk": {"value": 2, "grade": "fail"}, "high_risk_density": {"value": None, "grade": "na"}, } md = render_markdown_report(rd) assert "良好" in md assert "不合格" in md assert "不适用" in md def test_empty_report(self): md = render_markdown_report({}) assert "# 代码审查报告" in md assert "未发现问题" in md def test_line_unit_coverage_skips_file_count_rows(self): """gate=\"line+unit\" (feature) must NOT render the 全库/高风险 rows.""" rd = { "scope": "feature", "tier": "standard", "reviewed_files": ["src/a.py", "src/b.py"], "coverage": { "coverage_pct": None, "high_risk_coverage_pct": None, "grade": "good", "target_reached": True, "gate": "line+unit", "line_coverage_pct": 100.0, "unit_coverage_pct": 100.0, "line_gap_files": [], "unit_gap_files": [], }, } md = render_markdown_report(rd) assert "## 覆盖度" in md assert "**状态**" in md assert "100.0%" in md assert "覆盖度(全库)" not in md assert "覆盖度(高风险)" not in md def test_reviewed_files_collapsible(self): rd = { "scope": "feature", "tier": "standard", "reviewed_files": ["src/a.py", "src/b.py"], } md = render_markdown_report(rd) assert "审查文件(2 个)" in md assert "
" in md assert "点击展开 / 收起" in md assert "src/a.py" in md # flat "文件" meta line is skipped when reviewed_files is present assert "- **文件**:" not in md class TestGenerateReport: def test_both_writes_html_and_md(self, tmp_path): root = _mkroot(tmp_path) result = generate_report_func( _review_data(), output_path="sub/report", repo_root=str(root), ) assert result["status"] == "ok" assert isinstance(result["output_path"], list) assert len(result["files"]) == 2 html = root / "sub" / "report.html" md = root / "sub" / "report.md" assert html.is_file() assert md.is_file() html_text = html.read_text(encoding="utf-8") md_text = md.read_text(encoding="utf-8") assert "{{REPORT_DATA}}" not in html_text assert "代码审查报告" in html_text assert "\u2705 PASS" in html_text assert "N+1" in html_text assert "# 代码审查报告" in md_text def test_script_closing_sequence_is_escaped(self, tmp_path): """Literal "" in finding text must not break the HTML. A literal ```` inside the injected JSON data would close the surrounding opener', "path": "src/app.py", "line": 42, "confidence": 8, "fix": "escape before embedding", }, ] result = generate_report_func( rd, output_path="script-safe", repo_root=str(root), ) assert result["status"] == "ok" html_text = (root / "script-safe.html").read_text(encoding="utf-8") # The injected JSON payload must not contain a bare closing tag. data_block = html_text.split("const data = ", 1)[1].split(";", 1)[0] assert "") == 1 # Finding text is intact after JSON decoding. import json as _json decoded = _json.loads(data_block) assert "literal opener" in decoded["issues"][0]["message"] def test_markdown_only(self, tmp_path): root = _mkroot(tmp_path) result = generate_report_func( _review_data(), output_path="md-only", repo_root=str(root), format="markdown", ) assert result["status"] == "ok" assert len(result["files"]) == 1 assert str(result["output_path"]).endswith("md-only.md") assert (root / "md-only.md").is_file() assert not (root / "md-only.html").exists() def test_html_only(self, tmp_path): root = _mkroot(tmp_path) result = generate_report_func( _review_data(), output_path="html-only", repo_root=str(root), format="html", ) assert result["status"] == "ok" assert len(result["files"]) == 1 assert str(result["output_path"]).endswith("html-only.html") assert (root / "html-only.html").is_file() assert not (root / "html-only.md").exists() def test_default_output_path_is_repo_root(self, tmp_path): root = _mkroot(tmp_path) result = generate_report_func(_review_data(), repo_root=str(root)) assert result["status"] == "ok" assert (root / "code-review-report.html").is_file() assert (root / "code-review-report.md").is_file() def test_invalid_format_errors(self, tmp_path): root = _mkroot(tmp_path) result = generate_report_func( _review_data(), repo_root=str(root), format="xml", ) assert result["status"] == "error" assert "xml" in result["error"] def test_handles_missing_repo_root(self, tmp_path): with pytest.raises(ValueError): generate_report_func(_review_data(), repo_root=str(tmp_path / "missing"))