"""Unit tests for src/review_parse (ORCH-046). Covers the defensive extractors that pull verbatim must-fix text out of the reviewer / tester artifacts for embedding into the rollback ``task_desc``: - extract_review_findings (12-review.md, ## Findings -> P0/P1) - extract_test_failures (13-test-report.md, pytest/FAIL/Итог excerpt) Both must NEVER raise (return "" on missing/broken/empty input) and must ignore template placeholders / non-must-fix severities. See 04-test-plan.yaml (TC-01..08). """ import os import tempfile import pytest from src.review_parse import ( extract_review_findings, extract_test_failures, MAX_FINDINGS_CHARS, MAX_FAILURES_CHARS, ) @pytest.fixture def write_file(tmp_path): def _w(name: str, content: str) -> str: p = tmp_path / name p.write_text(content, encoding="utf-8") return str(p) return _w # --------------------------------------------------------------------------- # extract_review_findings # --------------------------------------------------------------------------- _REVIEW_WITH_FINDINGS = """--- type: review work_item_id: ORCH-046 verdict: REQUEST_CHANGES version: 1 --- # Review ORCH-046 ## Summary Несколько проблем. ## Findings ### P0 — Blocker - [ ] Документация не обновлена при изменении src/review_parse.py ### P1 — Must fix - [ ] extract_test_failures не обрабатывает пустой отчёт - [ ] Отсутствует docstring у _section_body ### P2 — Should fix - [ ] Переименовать переменную blocks в more descriptive ## Документация Требует обновления README. """ class TestExtractReviewFindings: def test_tc01_returns_verbatim_p0_p1(self, write_file): """TC-01: P0/P1 findings present -> verbatim text returned (AC-1, AC-5).""" path = write_file("12-review.md", _REVIEW_WITH_FINDINGS) out = extract_review_findings(path) # P0 + P1 verbatim items present. assert "Документация не обновлена при изменении src/review_parse.py" in out assert "extract_test_failures не обрабатывает пустой отчёт" in out assert "Отсутствует docstring у _section_body" in out # Subsection headers preserved. assert "P0" in out and "P1" in out # P2 must NOT leak in. assert "Переименовать переменную" not in out def test_tc02_only_p2_p3_returns_empty(self, write_file): """TC-02: only P2/P3 (no must-fix P0/P1) -> '' (AC-5).""" content = """--- verdict: REQUEST_CHANGES --- ## Findings ### P0 — Blocker - [ ] <описание> (если есть) ### P1 — Must fix - [ ] <описание> (если есть) ### P2 — Should fix - [ ] Косметика в naming """ path = write_file("12-review.md", content) assert extract_review_findings(path) == "" def test_tc03_missing_file_returns_empty(self): """TC-03: non-existent path -> '' without raising (AC-4).""" missing = os.path.join(tempfile.gettempdir(), "no-such-review-orch046.md") assert extract_review_findings(missing) == "" def test_tc04_broken_or_no_findings_section_returns_empty(self, write_file): """TC-04: empty file / markdown without ## Findings -> '' (AC-4, AC-5).""" # Empty file. assert extract_review_findings(write_file("empty.md", "")) == "" # No Findings section. no_section = "# Review\n\n## Summary\nвсё хорошо\n" assert extract_review_findings(write_file("nofind.md", no_section)) == "" # Broken YAML frontmatter (unterminated) — body parsing still graceful. broken = "---\nverdict: [unclosed\n# Review\nno findings here\n" assert extract_review_findings(write_file("broken.md", broken)) == "" def test_tc05_long_findings_truncated(self, write_file): """TC-05: very long findings truncated to limit with marker (AC-1).""" big_item = "- [ ] " + ("x" * 5000) content = f"## Findings\n\n### P0 — Blocker\n{big_item}\n" path = write_file("12-review.md", content) out = extract_review_findings(path) assert len(out) <= MAX_FINDINGS_CHARS + len("\n…(truncated)") assert "…(truncated)" in out def test_case_insensitive_and_dash_tolerant_header(self, write_file): """P0/P1 recognized regardless of case / dash style.""" content = """## Findings ### p0 - blocker - [ ] Нижний регистр заголовка ### P1 — Must fix - [ ] Em-dash заголовок """ out = extract_review_findings(write_file("12-review.md", content)) assert "Нижний регистр заголовка" in out assert "Em-dash заголовок" in out def test_never_raises_on_directory_path(self, tmp_path): """Passing a directory path must not raise -> ''.""" assert extract_review_findings(str(tmp_path)) == "" # --------------------------------------------------------------------------- # extract_test_failures # --------------------------------------------------------------------------- _REPORT_FAIL = """--- type: test-report work_item_id: ORCH-046 result: FAIL --- # Test Report — ORCH-046 ## Окружение - Python: 3.12 ## Результаты | TC ID | Описание | Результат | |-------|----------|-----------| | TC-01 | парсер findings | PASS | | TC-09 | rollback task_desc | FAIL | ## Вывод pytest FAILED tests/test_stage_engine.py::TestReviewerRequestChanges::test_embed - AssertionError 1 failed, 40 passed in 2.13s ## Итог FAIL """ class TestExtractTestFailures: def test_tc06_extracts_pytest_output(self, write_file): """TC-06: relevant body excerpt (pytest output) from FAIL report (AC-2, AC-5).""" path = write_file("13-test-report.md", _REPORT_FAIL) out = extract_test_failures(path) assert "FAILED tests/test_stage_engine.py" in out assert "1 failed, 40 passed" in out def test_priority_falls_back_to_fail_rows(self, write_file): """No pytest section -> FAIL rows of the results table are used.""" content = """--- result: FAIL --- ## Результаты | TC ID | Описание | Результат | |-------|----------|-----------| | TC-01 | ok | PASS | | TC-09 | broken | FAIL | ## Итог FAIL """ out = extract_test_failures(write_file("13-test-report.md", content)) assert "TC-09" in out assert "broken" in out # PASS rows are not failure-relevant. assert "TC-01" not in out def test_priority_falls_back_to_itog(self, write_file): """No pytest section and no FAIL rows -> Итог summary is used.""" content = """--- result: FAIL --- ## Итог Регресс упал: смотрите CI лог. """ out = extract_test_failures(write_file("13-test-report.md", content)) assert "Регресс упал" in out def test_tc07_missing_file_returns_empty(self): """TC-07: non-existent path -> '' without raising (AC-4).""" missing = os.path.join(tempfile.gettempdir(), "no-such-report-orch046.md") assert extract_test_failures(missing) == "" def test_tc08_broken_or_empty_report_returns_empty(self, write_file): """TC-08: empty / section-less report -> '' without raising (AC-4, AC-5).""" assert extract_test_failures(write_file("empty.md", "")) == "" no_sections = "---\nresult: FAIL\n---\n\n# Test Report\nничего полезного\n" assert extract_test_failures(write_file("nosec.md", no_sections)) == "" def test_long_failures_truncated(self, write_file): """Long pytest output is truncated to the limit with a marker.""" big = "x" * 5000 content = f"## Вывод pytest\n{big}\n" out = extract_test_failures(write_file("13-test-report.md", content)) assert len(out) <= MAX_FAILURES_CHARS + len("\n…(truncated)") assert "…(truncated)" in out def test_never_raises_on_directory_path(self, tmp_path): assert extract_test_failures(str(tmp_path)) == ""