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
34 changes: 34 additions & 0 deletions hackagent/cli/tui/views/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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_<judge_name>" 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"

Expand Down
100 changes: 100 additions & 0 deletions tests/integration/tui/results/test_widget_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down