chore: sync local changes, add Chinese docs and opencode config
This commit is contained in:
@@ -81,6 +81,61 @@ class TestBuildReportData:
|
||||
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):
|
||||
@@ -123,6 +178,45 @@ class TestRenderMarkdownReport:
|
||||
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 "<details>" 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):
|
||||
@@ -147,6 +241,47 @@ class TestGenerateReport:
|
||||
assert "N+1" in html_text
|
||||
assert "# 代码审查报告" in md_text
|
||||
|
||||
def test_script_closing_sequence_is_escaped(self, tmp_path):
|
||||
"""Literal "</script>" in finding text must not break the HTML.
|
||||
|
||||
A literal ``</script>`` inside the injected JSON data would close the
|
||||
surrounding <script> tag early, corrupting the report. The JSON payload
|
||||
must escape every "<" as "\\u003c" so the browser parses it as a plain
|
||||
string while the JS template re-renders it as "<".
|
||||
"""
|
||||
root = _mkroot(tmp_path)
|
||||
rd = _review_data()
|
||||
rd["findings"] = [
|
||||
{
|
||||
"severity": "major",
|
||||
"category": "security",
|
||||
"message": 'literal </script> opener',
|
||||
"path": "src/app.py",
|
||||
"line": 42,
|
||||
"confidence": 8,
|
||||
"fix": "escape </script> 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 "</script" not in data_block
|
||||
# It must keep the JSON escape so the value round-trips as "<".
|
||||
assert "\\u003c/script" in data_block
|
||||
# The real template closing tag must still be present exactly once.
|
||||
assert html_text.count("</script>") == 1
|
||||
# Finding text is intact after JSON decoding.
|
||||
import json as _json
|
||||
decoded = _json.loads(data_block)
|
||||
assert "literal </script> opener" in decoded["issues"][0]["message"]
|
||||
|
||||
def test_markdown_only(self, tmp_path):
|
||||
root = _mkroot(tmp_path)
|
||||
result = generate_report_func(
|
||||
|
||||
Reference in New Issue
Block a user