diff --git a/changes/325.fix b/changes/325.fix new file mode 100644 index 0000000..cec83f8 --- /dev/null +++ b/changes/325.fix @@ -0,0 +1 @@ +Phase status dots in the HTML report now reflect the structured verdict for verify and review phases. A verify phase with verdict ``FAIL`` shows a red dot even when its execution status is ``completed``; a review phase with verdict ``REWORK`` shows a yellow dot; and ``approve``/``pass``/``pass-with-follow-ups`` show a green dot. Hard execution failures (``failed``, ``skipped``, ``superseded``) still take priority over any verdict. diff --git a/src/raki/report/templates/report.html.j2 b/src/raki/report/templates/report.html.j2 index 59f4a73..100284d 100644 --- a/src/raki/report/templates/report.html.j2 +++ b/src/raki/report/templates/report.html.j2 @@ -1381,7 +1381,29 @@ {% if sorted_phases %}
{% for phase in sorted_phases %} - {% set dot_class = 'rework' if phase.status == 'completed' and phase.generation > 1 else phase.status %} + {# Compute dot_class: failed/skipped/superseded status always wins; for verify/review + phases with structured output, use verdict to pick the color; otherwise fall back to + the existing gen>1 → rework logic. #} + {% if phase.status in ('failed', 'skipped', 'superseded') %} + {% set dot_class = phase.status %} + {% elif phase.output_structured and phase.output_structured is mapping + and phase.output_structured.verdict is defined + and phase.name == 'verify' %} + {% set dot_class = 'completed' if phase.output_structured.verdict | lower == 'pass' else 'failed' %} + {% elif phase.output_structured and phase.output_structured is mapping + and phase.output_structured.verdict is defined + and phase.name == 'review' %} + {% set verdict_lc = phase.output_structured.verdict | lower %} + {% if verdict_lc in ('approve', 'pass', 'pass-with-follow-ups') %} + {% set dot_class = 'completed' %} + {% elif verdict_lc == 'rework' %} + {% set dot_class = 'rework' %} + {% else %} + {% set dot_class = 'failed' %} + {% endif %} + {% else %} + {% set dot_class = 'rework' if phase.status == 'completed' and phase.generation > 1 else phase.status %} + {% endif %}
{{ phase.name }} (gen {{ phase.generation }}) diff --git a/tests/test_report_html.py b/tests/test_report_html.py index a7df144..fb7d90e 100644 --- a/tests/test_report_html.py +++ b/tests/test_report_html.py @@ -2934,6 +2934,7 @@ def _make_report_with_phases(self, phases: list): generation=ph["generation"], status=ph["status"], output="done", + output_structured=ph.get("output_structured"), ) for ph in phases ] @@ -3025,6 +3026,257 @@ def test_superseded_phase_css_rule_defined(self, tmp_path: Path) -> None: # The CSS selector must be defined so the dot is actually styled. assert ".phase-status-superseded" in content + # --- Ticket #325: dot color should reflect verdict, not execution status --- + + def test_verify_pass_verdict_completed_gives_green_dot(self, tmp_path: Path) -> None: + """verify phase with verdict=PASS and status=completed should get green dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "verify", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "PASS"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-completed"' in content + assert 'class="phase-status phase-status-failed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_verify_fail_verdict_completed_gives_red_dot(self, tmp_path: Path) -> None: + """verify phase with verdict=FAIL and status=completed should get red dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "verify", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "FAIL"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-failed"' in content + assert 'class="phase-status phase-status-completed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_verify_pass_verdict_but_failed_status_gives_red_dot(self, tmp_path: Path) -> None: + """failed status takes priority over PASS verdict for verify phase.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "verify", + "generation": 1, + "status": "failed", + "output_structured": {"verdict": "PASS"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-failed"' in content + assert 'class="phase-status phase-status-completed"' not in content + + def test_verify_pass_verdict_but_skipped_status_gives_skipped_dot(self, tmp_path: Path) -> None: + """skipped status takes priority over PASS verdict for verify phase.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "verify", + "generation": 1, + "status": "skipped", + "output_structured": {"verdict": "PASS"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-skipped"' in content + assert 'class="phase-status phase-status-completed"' not in content + + def test_verify_pass_verdict_but_superseded_status_gives_superseded_dot( + self, tmp_path: Path + ) -> None: + """superseded status takes priority over PASS verdict for verify phase.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "verify", + "generation": 1, + "status": "superseded", + "output_structured": {"verdict": "PASS"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-superseded"' in content + assert 'class="phase-status phase-status-completed"' not in content + + def test_review_approve_verdict_gives_green_dot(self, tmp_path: Path) -> None: + """review phase with verdict=approve and status=completed should get green dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "approve"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-completed"' in content + assert 'class="phase-status phase-status-failed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_review_pass_verdict_gives_green_dot(self, tmp_path: Path) -> None: + """review phase with verdict=pass and status=completed should get green dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "pass"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-completed"' in content + assert 'class="phase-status phase-status-failed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_review_pass_with_follow_ups_verdict_gives_green_dot(self, tmp_path: Path) -> None: + """review phase with verdict=pass-with-follow-ups should get green dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "pass-with-follow-ups"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-completed"' in content + assert 'class="phase-status phase-status-failed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_review_rework_verdict_gives_yellow_dot(self, tmp_path: Path) -> None: + """review phase with verdict=REWORK and status=completed should get yellow rework dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "REWORK"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-rework"' in content + assert 'class="phase-status phase-status-completed"' not in content + assert 'class="phase-status phase-status-failed"' not in content + + def test_review_fail_verdict_gives_red_dot(self, tmp_path: Path) -> None: + """review phase with verdict=FAIL and status=completed should get red dot.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "completed", + "output_structured": {"verdict": "FAIL"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-failed"' in content + assert 'class="phase-status phase-status-completed"' not in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_review_rework_verdict_but_failed_status_gives_red_dot(self, tmp_path: Path) -> None: + """failed status takes priority over REWORK verdict for review phase.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "review", + "generation": 1, + "status": "failed", + "output_structured": {"verdict": "REWORK"}, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-failed"' in content + assert 'class="phase-status phase-status-rework"' not in content + + def test_non_verify_review_phase_uses_generation_logic(self, tmp_path: Path) -> None: + """implement phase with gen=1 should still use existing green/rework logic.""" + from raki.report.html_report import write_html_report + + report = self._make_report_with_phases( + [ + { + "name": "implement", + "generation": 2, + "status": "completed", + "output_structured": None, + } + ] + ) + output = tmp_path / "report.html" + write_html_report(report, output, include_sessions=True) + content = output.read_text() + assert 'class="phase-status phase-status-rework"' in content + assert 'class="phase-status phase-status-completed"' not in content + # --- Ticket #250: Structured drill-down sections ---