diff --git a/hackagent/cli/tui/views/results.py b/hackagent/cli/tui/views/results.py index 93f956f9..818c95d5 100644 --- a/hackagent/cli/tui/views/results.py +++ b/hackagent/cli/tui/views/results.py @@ -1408,6 +1408,8 @@ def _show_result_summary(self, run: Any) -> None: asr = float(eval_summary.get("overall_success_rate", 0.0) or 0.0) * 100.0 mv_asr = float(eval_summary.get("majority_vote_asr", 0.0) or 0.0) * 100.0 fleiss = eval_summary.get("fleiss_kappa") + strictness = eval_summary.get("per_judge_strictness") + is_multi_judge = bool(eval_summary.get("is_multi_judge")) summary = ( f"[bold cyan]▌ Selected Run[/bold cyan]\n" @@ -1428,6 +1430,38 @@ def _show_result_summary(self, run: Any) -> None: except (TypeError, ValueError): summary += f" Fleiss κ: [bold]{_escape(str(fleiss))}[/bold]" summary += "\n" + + if is_multi_judge and isinstance(strictness, dict): + judge_keys = [k for k in strictness.keys() if k != "bias_gap"] + if judge_keys: + parts = [] + # Judge columns follow the "eval_" naming + # convention (see _is_canonical_eval_vote_column in + # hackagent/attacks/evaluator/metrics.py); sorted for a + # stable, deterministic display order. + for jk in sorted(judge_keys): + try: + val = float(strictness.get(jk, 0.0) or 0.0) + judge_name = _escape( + jk.replace("eval_", "").replace("_", " ") + ) + parts.append(f"{judge_name}: [bold]{val:.3f}[/bold]") + except (TypeError, ValueError): + continue + if parts: + bias_gap = strictness.get("bias_gap") + bias_gap_str = "" + if bias_gap is not None: + try: + bias_gap_str = ( + f" Bias gap: [bold]{float(bias_gap):.3f}[/bold]" + ) + except (TypeError, ValueError): + pass + summary += ( + f" [dim]Strictness — {' '.join(parts)}[/dim]" + f"{bias_gap_str}\n" + ) else: summary += "\n[dim]No evaluation summary synced yet for this run.[/dim]\n" diff --git a/tests/integration/tui/results/test_widget_lifecycle.py b/tests/integration/tui/results/test_widget_lifecycle.py index a4c34853..dfeccbb1 100644 --- a/tests/integration/tui/results/test_widget_lifecycle.py +++ b/tests/integration/tui/results/test_widget_lifecycle.py @@ -503,6 +503,106 @@ def compose(self): assert len(container.children) == 0 +class TestResultsRunSummary: + """ + Test suite for the run summary header panel, including multi-judge + metrics (Majority ASR, Fleiss Kappa, per-judge strictness, bias gap). + """ + + @pytest.mark.asyncio + async def test_show_result_summary_includes_multi_judge_metrics(self, cli_config): + """ + Test that _show_result_summary renders multi-judge metrics. + + When the run's evaluation_summary indicates multiple judges were + used, the header should display Majority ASR, Fleiss Kappa, + per-judge strictness values, and the bias gap. + """ + + class TestApp(App): + def compose(self): + yield ResultsTab(cli_config) + + app = TestApp() + async with app.run_test() as _: + tab = app.query_one(ResultsTab) + + run = Mock() + run.id = uuid4() + run.status = Mock(value="COMPLETED") + run.timestamp = datetime(2026, 1, 19, 11, 0, 0) + run.run_config = { + "evaluation_summary": { + "total_attacks": 10, + "overall_success_rate": 0.5, + "majority_vote_asr": 0.4, + "fleiss_kappa": 0.75, + "is_multi_judge": True, + "per_judge_strictness": { + "eval_judge_a": 0.2, + "eval_judge_b": 0.6, + "bias_gap": 0.4, + }, + } + } + + tab._show_result_summary(run) + + header = tab.query_one("#run-header-static", Static) + rendered = str(header.render()) + + assert "Majority ASR" in rendered + assert "40.0%" in rendered + assert "Fleiss" in rendered + assert "0.750" in rendered + assert "Strictness" in rendered + assert "judge a" in rendered + assert "judge b" in rendered + assert "Bias gap" in rendered + assert "0.400" in rendered + + @pytest.mark.asyncio + async def test_show_result_summary_hides_strictness_for_single_judge( + self, cli_config + ): + """ + Test that per-judge strictness/bias gap are omitted when only a + single judge is present (is_multi_judge is False). + """ + + class TestApp(App): + def compose(self): + yield ResultsTab(cli_config) + + app = TestApp() + async with app.run_test() as _: + tab = app.query_one(ResultsTab) + + run = Mock() + run.id = uuid4() + run.status = Mock(value="COMPLETED") + run.timestamp = datetime(2026, 1, 19, 11, 0, 0) + run.run_config = { + "evaluation_summary": { + "total_attacks": 5, + "overall_success_rate": 0.2, + "majority_vote_asr": 0.2, + "fleiss_kappa": 1.0, + "is_multi_judge": False, + "per_judge_strictness": {"bias_gap": 0.0}, + } + } + + tab._show_result_summary(run) + + header = tab.query_one("#run-header-static", Static) + rendered = str(header.render()) + + assert "Majority ASR" in rendered + assert "Strictness" not in rendered + assert "Bias gap" not in rendered + + class TestResultsPagination: """ Test suite for pagination functionality.