headroom/tests/test_cli_perf_format.py
Michael Sam d2cdab268d
feat(proxy): add agent-90 savings profile (#830)
## Summary
- add an `agent-90` savings profile with cross-agent proxy env exports
- wire the profile into proxy/router runtime kwargs, including
force-Kompress routing and a smaller read-protection window
- expose effective savings-profile config in `/stats` and add focused
regression coverage

## Type of change
- [x] feat (non-breaking change which adds functionality)
- [ ] fix (non-breaking change which fixes an issue)
- [ ] docs
- [ ] test/CI-only
- [ ] refactor-only

## Testing
- [x] `python3 -m py_compile headroom/agent_savings.py
headroom/cli/agent_savings.py headroom/cli/main.py headroom/cli/proxy.py
headroom/proxy/models.py headroom/proxy/server.py
headroom/transforms/content_router.py tests/test_agent_savings.py
tests/test_proxy_healthchecks.py tests/test_cli/test_wrap_persistent.py
tests/test_transforms/test_content_router.py`
- [x] `git diff --check`
- [x] manual smoke: `agent-savings --profile agent-90 --format json`
returns `HEADROOM_TARGET_RATIO=0.10`
- [x] manual smoke:
`proxy_pipeline_kwargs(ProxyConfig(savings_profile="agent-90"))` enables
`force_kompress`, system/user compression, and
`read_protection_window=2`
- [x] manual smoke: Anthropic-style `tool_result` routes through
Kompress with `target_ratio=0.10`
- [ ] `pytest` suite not run: pytest is not installed in the available
local Python environments

## Notes
This keeps agent-90 as an opt-in profile. Existing defaults remain
unchanged unless `HEADROOM_SAVINGS_PROFILE=agent-90` or
`ProxyConfig(savings_profile="agent-90")` is set.
2026-06-11 18:58:06 -05:00

193 lines
6.5 KiB
Python

"""Tests for `headroom perf --format {text,json,csv}` (issue #595)."""
from __future__ import annotations
import csv
import io
import json
import pytest
from click.testing import CliRunner
from headroom.cli.main import main
from headroom.perf import analyzer
from headroom.perf.analyzer import (
PerfRecord,
PerfReport,
TransformRecord,
build_perf_summary,
perf_records_as_dicts,
)
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
def _sample_report() -> PerfReport:
"""A small report with two models, cache numbers, and a transform."""
return PerfReport(
perf_records=[
PerfRecord(
timestamp="2026-06-05 10:00:00,000",
request_id="hr_1",
model="claude-sonnet-4.5",
num_messages=10,
tokens_before=1000,
tokens_after=400,
tokens_saved=600,
cache_read=800,
cache_write=200,
cache_hit_pct=80,
optimization_ms=12.0,
transforms=["content_router"],
),
PerfRecord(
timestamp="2026-06-05 11:00:00,000",
request_id="hr_2",
model="claude-opus-4-8",
num_messages=4,
tokens_before=1000,
tokens_after=600,
tokens_saved=400,
cache_read=200,
cache_write=0,
cache_hit_pct=100,
optimization_ms=8.0,
transforms=["content_router"],
),
],
transform_records=[
TransformRecord(
timestamp="2026-06-05 10:00:00,000",
name="content_router",
tokens_before=2000,
tokens_after=1000,
tokens_saved=1000,
),
],
log_files_read=1,
total_lines_parsed=42,
requested_hours=24.0,
oldest_kept_ts="2026-06-05 10:00:00,000",
newest_kept_ts="2026-06-05 11:00:00,000",
)
# ---------------------------------------------------------------------------
# Pure builders
# ---------------------------------------------------------------------------
def test_build_perf_summary_totals_and_pct():
summary = build_perf_summary(_sample_report())
assert summary["total_requests"] == 2
assert summary["total_tokens_before"] == 2000
assert summary["total_tokens_after"] == 1000
assert summary["tokens_saved"] == 1000
# 1000 / 2000 == 50.0%
assert summary["savings_pct"] == 50.0
# cache: read 1000, write 200 -> 1000 / 1200 == 83.3%
assert summary["cache_read_tokens"] == 1000
assert summary["cache_write_tokens"] == 200
assert summary["cache_hit_pct"] == 83.3
assert summary["window_hours"] == 24.0
def test_build_perf_summary_by_model_and_transform():
summary = build_perf_summary(_sample_report())
models = {m["model"]: m for m in summary["by_model"]}
assert set(models) == {"claude-sonnet-4.5", "claude-opus-4-8"}
assert models["claude-sonnet-4.5"]["tokens_saved"] == 600
assert models["claude-sonnet-4.5"]["savings_pct"] == 60.0
assert models["claude-opus-4-8"]["savings_pct"] == 40.0
assert summary["by_transform"][0]["transform"] == "content_router"
assert summary["by_transform"][0]["tokens_saved"] == 1000
assert summary["by_transform"][0]["uses"] == 1
def test_build_perf_summary_empty_report_no_zero_division():
summary = build_perf_summary(PerfReport(requested_hours=168.0))
assert summary["total_requests"] == 0
assert summary["savings_pct"] == 0.0
assert summary["cache_hit_pct"] == 0.0
assert summary["by_model"] == []
def test_perf_records_as_dicts_roundtrips_fields():
dicts = perf_records_as_dicts(_sample_report())
assert len(dicts) == 2
assert dicts[0]["request_id"] == "hr_1"
assert dicts[0]["tokens_saved"] == 600
# transforms stays a list for JSON consumers
assert dicts[0]["transforms"] == ["content_router"]
# ---------------------------------------------------------------------------
# CLI integration
# ---------------------------------------------------------------------------
def _patch_report(monkeypatch, report: PerfReport) -> None:
monkeypatch.setattr(analyzer, "parse_log_files", lambda last_n_hours=168.0: report)
def test_perf_json_format(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "json"])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert data["savings_pct"] == 50.0
assert "by_model" in data
assert data["total_requests"] == 2
def test_perf_json_raw_is_array(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "json", "--raw"])
assert result.exit_code == 0, result.output
data = json.loads(result.output)
assert isinstance(data, list)
assert len(data) == 2
assert data[0]["request_id"] == "hr_1"
def test_perf_csv_by_model(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "csv"])
assert result.exit_code == 0, result.output
rows = list(csv.DictReader(io.StringIO(result.output)))
assert {r["model"] for r in rows} == {"claude-sonnet-4.5", "claude-opus-4-8"}
sonnet = next(r for r in rows if r["model"] == "claude-sonnet-4.5")
assert sonnet["tokens_saved"] == "600"
def test_perf_csv_raw_per_record(runner, monkeypatch):
report = _sample_report()
report.perf_records[0].client = "codex"
_patch_report(monkeypatch, report)
result = runner.invoke(main, ["perf", "--format", "csv", "--raw"])
assert result.exit_code == 0, result.output
rows = list(csv.DictReader(io.StringIO(result.output)))
assert len(rows) == 2
assert rows[0]["request_id"] == "hr_1"
assert rows[0]["client"] == "codex"
# transforms flattened to a string cell
assert rows[0]["transforms"] == "content_router"
def test_perf_text_default_unchanged(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf"])
assert result.exit_code == 0, result.output
assert "Headroom Performance Report" in result.output
def test_perf_rejects_unknown_format(runner, monkeypatch):
_patch_report(monkeypatch, _sample_report())
result = runner.invoke(main, ["perf", "--format", "xml"])
assert result.exit_code != 0