Files
OpenJarvis/tests/evals/test_use_case_benchmarks.py
T
28ef745384 fix(leaderboard): correct telemetry pipeline and outlier handling (#498)
* fix(leaderboard): correct telemetry pipeline and outlier handling

The public leaderboard at /leaderboard showed a clear bimodal Wh/token
distribution: most users at ~3-5 J/token, ~30% inflated by 1000-4000×.
A 5-agent investigation workflow + 4-agent verification pass traced the
inflation to a cluster of related bugs across telemetry, server, and
display layers. This PR fixes the in-repo half. Backfilling existing
Supabase data is a separate follow-up.

Bug 1 — Dual telemetry recording (`server/routes.py`)
=====================================================

`_handle_direct` wrapped the engine with `instrumented_generate`
unconditionally. When `app.state.engine` was already an
`InstrumentedEngine` (the common case when telemetry is wired in),
BOTH layers published `TELEMETRY_RECORD` — once from the inner
`InstrumentedEngine.generate`, once from the outer wrapper. Every
chat-completion request was counted twice in the leaderboard pipeline.

Fix: detect the InstrumentedEngine and unwrap to `._inner` before
passing to the wrapper so only one layer fires.

Bug 2 — KV-cache fallback over-counts multi-turn (`server/savings.py`)
=====================================================================

`compute_savings` falls back from `prompt_tokens_evaluated` to
`prompt_tokens` when the KV-cache-aware count is missing. But routes.py
aggregates by summing each turn's full prompt — which counts the system
prompt N times for an N-turn conversation. The fallback inflated FLOPs
and energy by N×.

Fix: use 0 (conservative under-count) when the evaluated count is
missing rather than falling back to the inflated `prompt_tokens` sum.

Bug 3 — TelemetryRecord lacked methodology versioning
=====================================================

There was no per-record version tag, so legacy (pre-fix) and current
records were silently aggregated together in the public leaderboard
even though they used different methodologies.

Fix: add `token_counting_version: Optional[int]` field to the
`TelemetryRecord` dataclass + a nullable column to the SQLite schema
(with idempotent migration); the constant moves from `server.savings`
to `core.types` to avoid the server→telemetry layering. New records
write the current version; pre-fix rows remain NULL. The aggregator
gains a `current_methodology_only=True` flag that filters NULL rows out
of leaderboard sums — local dashboards leave it off so historical
aggregates still render.

Bug 4 — Leaderboard JS displayed missing telemetry as legit zeros
=================================================================

Rows with significant token counts but `energy_wh_saved = 0` and
`flops_saved = 0` rendered as `0.00 Wh / 0 FLOPs` — visually identical
to a user who genuinely did almost nothing. The headline totals also
included these rows.

Fix: new `isMissingTelemetry()` detector renders missing-telemetry
energy/FLOPs cells as `—` (with a tooltip explaining why); a new
`.lb-missing` CSS class differentiates the placeholder visually.

Bug 5 — Outlier filter was too generous
=======================================

`MAX_ENERGY_WH_PER_TOKEN = 10` left a 10,000× margin that admitted
every Group B row even though those values are physically impossible
(would imply a space-heater per token). Same for the FLOPs cap at
`1e17`.

Fix: tighten to `0.5` Wh/token (still 500× over a typical consumer GPU)
and `1e15` FLOPs/token (still 10,000× over typical). This removes
existing pre-fix corrupt rows from the public view without touching
Supabase.

Tests
=====

New regression tests (all pass, ruff clean):

- `tests/server/test_routes.py::test_instrumented_engine_unwrapped_to_avoid_dual_telemetry`
  — pins the bug 1 fix; asserts exactly ONE TELEMETRY_RECORD event per
  request when the engine is already an InstrumentedEngine.
- `tests/server/test_savings.py` (NEW file, 3 tests) — pins the bug 2
  fix (FLOPs not inflated via fallback) and the cost-side invariant
  (dollar savings still use full prompt_tokens because cloud providers
  bill per input token even when local KV cache hit).
- `tests/telemetry/test_aggregator.py::TestMethodologyFilter` (3 tests)
  — default behaviour includes legacy rows (local dashboard parity);
  `current_methodology_only=True` excludes them; the summary surface
  honors the same filter.

Unrelated test note
===================

`tests/telemetry/test_energy_wiring.py::TestTelemetryStatsEnergy::test_export_includes_energy_fields`
and `::TestEndToEndPipeline::test_ask_to_export_with_energy` are flaky
on this branch but ALSO flaky on `main` (confirmed via stash + the
banner-only run). Root cause: `_version_check.py:132-135` prints the
"new version of OpenJarvis is available" banner to stdout outside the
throttle window, contaminating `json.loads(result.output)` in those two
tests. Reproducible with `OPENJARVIS_NO_UPDATE_CHECK=1` → all pass.
That's a separate bug (the banner shouldn't write to the same stream
as machine-readable output); not addressing it here to keep this PR
focused on the leaderboard pipeline.

What this PR explicitly does NOT do
====================================

- Backfill ~30% of Supabase rows that already shipped with inflated
  values from pre-fix clients. That requires Supabase write access and
  is the natural follow-up — see PR comments for proposed dry-run audit
  query and the additive `methodology_version` column migration.
- Distinguish which past submissions came from buggy vs correct clients
  retroactively (no `app_version` tag on existing submissions).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(evals): fix test_energy_scales_linearly under leaderboard fix

CI on #498 failed in the slow `test` job:

    tests/evals/test_use_case_benchmarks.py::TestSavings::
    test_energy_scales_linearly — ZeroDivisionError: float division by zero

Root cause: the test (and `test_energy_wh_matches_direct_formula`)
called `compute_savings(N, 0)` without `prompt_tokens_evaluated`. Under
the OLD buggy fallback, an unset evaluated count silently became N,
energy scaled linearly with N, and the test passed by accident. Under
this branch's conservative fix the fallback is 0, FLOPs collapse to 0,
and the `p10.energy_wh / p1.energy_wh` ratio divides 0 by 0.

The test's own docstring says "evaluated tokens (KV-cache model)" — it
was always meant to exercise the explicit-evaluated path, the API
call just didn't match. Pass `prompt_tokens_evaluated` explicitly so
the test now expresses the invariant it claims to.

Same one-line fix for `test_energy_wh_matches_direct_formula` plus an
explicit `flops > 0` sanity assertion — that test was passing trivially
with `0 == 0` after the conservative fallback fix, masking whether the
formula was actually being exercised.

No production code change in this commit. Verified locally: all 6
TestSavings tests pass; the 5 new regression tests added on this branch
still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: krypticmouse <herumbshandilya123@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 16:59:24 -07:00

484 lines
17 KiB
Python

"""Tests for the 5 use-case benchmark datasets and scorers.
Tests verify:
1. Each dataset can be instantiated with correct attributes
2. Each dataset loads synthetic records (no HuggingFace download)
3. Records have expected fields
4. Each scorer can be instantiated
5. Coding task scorer can score a correct answer
6. Email triage scorer handles exact match
7. CLI factory functions work for all 5 benchmarks
8. Cost calculator and savings modules work
"""
from __future__ import annotations
from unittest.mock import MagicMock # noqa: I001
import pytest
# ---------------------------------------------------------------------------
# Dataset instantiation and loading
# ---------------------------------------------------------------------------
class TestEmailTriageDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
assert ds.dataset_id == "email_triage"
assert ds.dataset_name == "Email Triage"
def test_load(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
records = list(ds.iter_records())
assert len(records) == 5
r = records[0]
assert r.record_id.startswith("email-triage-")
assert r.category == "use-case"
assert "urgency" in r.metadata
assert "category" in r.metadata
def test_load_all(self) -> None:
from openjarvis.evals.datasets.email_triage import EmailTriageDataset
ds = EmailTriageDataset()
ds.load()
assert ds.size() == 30
class TestMorningBriefDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
assert ds.dataset_id == "morning_brief"
assert ds.dataset_name == "Morning Brief"
def test_load(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
records = list(ds.iter_records())
r = records[0]
assert r.record_id.startswith("morning-brief-")
assert r.reference # key_priorities not empty
def test_load_all(self) -> None:
from openjarvis.evals.datasets.morning_brief import MorningBriefDataset
ds = MorningBriefDataset()
ds.load()
assert ds.size() == 15
class TestResearchMiningDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
assert ds.dataset_id == "research_mining"
assert ds.dataset_name == "Research Mining"
def test_load(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
records = list(ds.iter_records())
r = records[0]
assert r.record_id.startswith("research-mining-")
assert "domain" in r.metadata
def test_load_all(self) -> None:
from openjarvis.evals.datasets.research_mining import ResearchMiningDataset
ds = ResearchMiningDataset()
ds.load()
assert ds.size() == 31
class TestKnowledgeBaseDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
assert ds.dataset_id == "knowledge_base"
assert ds.dataset_name == "Knowledge Base"
def test_load(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
records = list(ds.iter_records())
r = records[0]
assert r.record_id.startswith("knowledge-base-")
assert r.reference # answer not empty
def test_load_all(self) -> None:
from openjarvis.evals.datasets.knowledge_base import KnowledgeBaseDataset
ds = KnowledgeBaseDataset()
ds.load()
assert ds.size() == 30
class TestCodingTaskDataset:
def test_instantiation(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
assert ds.dataset_id == "coding_task"
assert ds.dataset_name == "Coding Task"
def test_load(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
ds.load(max_samples=5, seed=42)
assert ds.size() == 5
records = list(ds.iter_records())
r = records[0]
assert r.record_id.startswith("coding-task-")
assert "test_cases" in r.metadata
assert "signature" in r.metadata
def test_load_all(self) -> None:
from openjarvis.evals.datasets.coding_task import CodingTaskDataset
ds = CodingTaskDataset()
ds.load()
assert ds.size() == 29
# ---------------------------------------------------------------------------
# Scorer instantiation
# ---------------------------------------------------------------------------
class TestScorerInstantiation:
def _mock_backend(self):
return MagicMock()
def test_email_triage_scorer(self) -> None:
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
scorer = EmailTriageScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "email_triage"
def test_morning_brief_scorer(self) -> None:
from openjarvis.evals.scorers.morning_brief import MorningBriefScorer
scorer = MorningBriefScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "morning_brief"
def test_research_mining_scorer(self) -> None:
from openjarvis.evals.scorers.research_mining import ResearchMiningScorer
scorer = ResearchMiningScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "research_mining"
def test_knowledge_base_scorer(self) -> None:
from openjarvis.evals.scorers.knowledge_base import KnowledgeBaseScorer
scorer = KnowledgeBaseScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "knowledge_base"
def test_coding_task_scorer(self) -> None:
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
scorer = CodingTaskScorer(self._mock_backend(), "gpt-5-mini")
assert scorer.scorer_id == "coding_task"
# ---------------------------------------------------------------------------
# Scorer functional tests
# ---------------------------------------------------------------------------
class TestCodingTaskScoring:
"""Test the coding task scorer with actual code execution."""
def test_correct_answer(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
scorer = CodingTaskScorer()
record = EvalRecord(
record_id="test-1",
problem="Write is_palindrome",
reference="",
category="use-case",
subject="coding_task",
metadata={
"test_cases": (
'assert is_palindrome("racecar") == True\n'
'assert is_palindrome("hello") == False\n'
'assert is_palindrome("") == True'
),
},
)
answer = "def is_palindrome(s):\n return s == s[::-1]"
is_correct, meta = scorer.score(record, answer)
assert is_correct is True
assert meta["tests_passed"] == 3
assert meta["pass_rate"] == 1.0
def test_incorrect_answer(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
scorer = CodingTaskScorer()
record = EvalRecord(
record_id="test-2",
problem="Write add",
reference="",
category="use-case",
metadata={
"test_cases": ("assert add(1, 2) == 3\nassert add(0, 0) == 0"),
},
)
# Wrong implementation
answer = "def add(a, b):\n return a * b"
is_correct, meta = scorer.score(record, answer)
assert is_correct is False
def test_empty_answer(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.coding_task import CodingTaskScorer
scorer = CodingTaskScorer()
record = EvalRecord(
record_id="test-3",
problem="Write something",
reference="",
category="use-case",
metadata={"test_cases": "assert True"},
)
is_correct, meta = scorer.score(record, "")
assert is_correct is False
assert meta["reason"] == "empty_response"
class TestEmailTriageScoring:
"""Test exact-match path of email triage scorer."""
def test_exact_match(self) -> None:
from openjarvis.evals.core.types import EvalRecord
from openjarvis.evals.scorers.email_triage import EmailTriageScorer
scorer = EmailTriageScorer(MagicMock(), "gpt-5-mini")
record = EvalRecord(
record_id="test-1",
problem="...",
reference="urgency: high\ncategory: action",
category="use-case",
metadata={"urgency": "high", "category": "action"},
)
answer = "urgency: high\ncategory: action\ndraft: I'll look into this."
is_correct, meta = scorer.score(record, answer)
assert is_correct is True
assert meta["match_type"] == "exact"
# ---------------------------------------------------------------------------
# CLI factory tests
# ---------------------------------------------------------------------------
class TestCLIFactories:
"""Test that _build_dataset and _build_scorer work for new benchmarks."""
@pytest.mark.parametrize(
"benchmark",
[
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
],
)
def test_build_dataset(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_dataset
ds = _build_dataset(benchmark)
assert ds.dataset_id == benchmark
@pytest.mark.parametrize(
"benchmark",
[
"email_triage",
"morning_brief",
"research_mining",
"knowledge_base",
"coding_task",
],
)
def test_build_scorer(self, benchmark: str) -> None:
from openjarvis.evals.cli import _build_scorer
scorer = _build_scorer(benchmark, MagicMock(), "gpt-5-mini")
assert scorer.scorer_id == benchmark
# ---------------------------------------------------------------------------
# Cost calculator tests
# ---------------------------------------------------------------------------
class TestCostCalculator:
"""Test the cost calculator module."""
def test_estimate_monthly_cost(self) -> None:
from openjarvis.server.cost_calculator import estimate_monthly_cost
est = estimate_monthly_cost(
calls_per_month=1000,
avg_input_tokens=500,
avg_output_tokens=200,
provider_key="gpt-5.3",
)
assert est.monthly_cost > 0
assert est.annual_cost == est.monthly_cost * 12
assert est.total_calls_per_month == 1000
def test_estimate_scenario(self) -> None:
from openjarvis.server.cost_calculator import estimate_scenario
estimates = estimate_scenario("daily_briefing")
assert len(estimates) == 3 # 3 cloud providers
for est in estimates:
assert est.monthly_cost > 0
def test_estimate_all_scenarios(self) -> None:
from openjarvis.server.cost_calculator import estimate_all_scenarios
all_est = estimate_all_scenarios()
assert len(all_est) == 5 # 5 scenarios
def test_unknown_provider(self) -> None:
from openjarvis.server.cost_calculator import estimate_monthly_cost
with pytest.raises(ValueError, match="Unknown provider"):
estimate_monthly_cost(100, 100, 100, "nonexistent")
def test_unknown_scenario(self) -> None:
from openjarvis.server.cost_calculator import estimate_scenario
with pytest.raises(ValueError, match="Unknown scenario"):
estimate_scenario("nonexistent")
# ---------------------------------------------------------------------------
# Savings module tests
# ---------------------------------------------------------------------------
class TestSavings:
"""Test the savings computation module."""
def test_compute_savings_basic(self) -> None:
from openjarvis.server.savings import compute_savings
summary = compute_savings(1000, 500, total_calls=10)
assert summary.total_calls == 10
assert summary.total_tokens == 1500
assert summary.local_cost == 0.0
assert len(summary.per_provider) == 3
for p in summary.per_provider:
assert p.total_cost > 0
def test_compute_savings_with_session(self) -> None:
import time
from openjarvis.server.savings import compute_savings
start = time.time() - 3600 # 1 hour ago
summary = compute_savings(
100000,
50000,
total_calls=100,
session_start=start,
)
assert summary.session_duration_hours > 0
assert summary.monthly_projection # not empty
def test_savings_to_dict(self) -> None:
from openjarvis.server.savings import compute_savings, savings_to_dict
summary = compute_savings(1000, 500, total_calls=5)
d = savings_to_dict(summary)
assert isinstance(d, dict)
assert "per_provider" in d
assert "total_calls" in d
def test_energy_scales_linearly(self) -> None:
"""Energy should scale linearly with evaluated tokens (KV-cache model).
The test must pass `prompt_tokens_evaluated` explicitly — under the
leaderboard-correctness fix, `compute_savings` no longer falls back
from a missing `prompt_tokens_evaluated` to the (multi-turn-inflated)
`prompt_tokens` sum. When the KV-cache-aware count is missing, FLOPs
derive from `completion_tokens` only. With completion=0, that path
produces zero energy and divides by zero. Passing the evaluated
count explicitly is the API contract that lets this invariant be
tested without depending on the (now intentionally conservative)
fallback behaviour.
"""
from openjarvis.server.savings import compute_savings
s1 = compute_savings(1000, 0, prompt_tokens_evaluated=1000)
s10 = compute_savings(10000, 0, prompt_tokens_evaluated=10000)
# FLOPs = 2*P*T (linear), so 10x tokens => 10x FLOPs => 10x energy
for p1, p10 in zip(s1.per_provider, s10.per_provider):
ratio = p10.energy_wh / p1.energy_wh
assert 9 < ratio < 11, (
f"{p1.provider}: energy ratio {ratio:.1f}, expected ~10"
)
def test_energy_wh_matches_direct_formula(self) -> None:
"""Energy must equal flops * wh_per_flop for known constants.
Pass `prompt_tokens_evaluated` explicitly for the same reason as
`test_energy_scales_linearly` — the conservative-fallback fix in
savings.py means an unset `prompt_tokens_evaluated` produces 0
FLOPs and would make this test pass trivially with 0 == 0.
"""
from openjarvis.server.savings import CLOUD_PRICING, compute_savings
summary = compute_savings(10000, 0, prompt_tokens_evaluated=10000)
for p in summary.per_provider:
pricing = CLOUD_PRICING[p.provider]
wh_per_flop = pricing["energy_wh_per_1k_tokens"] / (
1000 * pricing.get("flops_per_token", 3e12)
)
expected = p.flops * wh_per_flop
assert abs(p.energy_wh - expected) < 1e-6, (
f"{p.provider}: energy_wh={p.energy_wh}, expected={expected}"
)
# Sanity: assert the formula isn't matching trivially with both
# sides equal to zero (would happen if FLOPs collapse to 0).
assert p.flops > 0, (
f"{p.provider}: FLOPs must be positive when "
f"prompt_tokens_evaluated is explicitly set"
)
def test_energy_not_zero(self) -> None:
"""Energy must be positive for non-zero token counts."""
from openjarvis.server.savings import compute_savings
summary = compute_savings(500, 500)
for p in summary.per_provider:
assert p.energy_wh > 0, f"{p.provider}: energy_wh should be > 0"