Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/325.fix
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 23 additions & 1 deletion src/raki/report/templates/report.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,29 @@
{% if sorted_phases %}
<div class="phase-list">
{% 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 %}
<div class="phase-item">
<span class="phase-status phase-status-{{ dot_class }}"></span>
<span>{{ phase.name }} (gen {{ phase.generation }})</span>
Expand Down
252 changes: 252 additions & 0 deletions tests/test_report_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand Down Expand Up @@ -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 ---

Expand Down
Loading