| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- import importlib
- import importlib.util
- import json
- import tomllib
- from datetime import datetime, timezone
- from io import StringIO
- from pathlib import Path
- from types import SimpleNamespace
- import pytest
- from agent_lab.application.benchmark import (
- BenchmarkCaseId,
- BenchmarkMode,
- BenchmarkRunResult,
- )
- def _cli_module():
- spec = importlib.util.find_spec("agent_lab.presentation.benchmark_cli")
- assert spec is not None, "benchmark_cli module must be implemented"
- return importlib.import_module("agent_lab.presentation.benchmark_cli")
- def _write_config(path: Path, **overrides: object) -> Path:
- payload = {
- "schema_version": 1,
- "base_url": "https://json.example/v1",
- "model": "json-model",
- "runs_per_case": 1,
- "cases": ["ordinary_chat"],
- "modes": ["dual_agent"],
- }
- payload.update(overrides)
- path.write_text(json.dumps(payload))
- return path
- def _run_result(
- *,
- case_id: BenchmarkCaseId = BenchmarkCaseId.ORDINARY_CHAT,
- mode: BenchmarkMode = BenchmarkMode.DUAL_AGENT,
- iteration: int = 1,
- status: str = "passed",
- error: str | None = None,
- ) -> BenchmarkRunResult:
- return BenchmarkRunResult(
- case_id=case_id,
- mode=mode,
- iteration=iteration,
- status=status,
- initial_provider_ttft_ms=1,
- visible_ttft_ms=2,
- turn_wall_time_ms=3,
- prompt_tokens=2,
- completion_tokens=3,
- total_tokens=5,
- cached_tokens=0,
- model_call_count=1,
- fallback_count=0,
- tool_count=0,
- event_names=[],
- batch_event_names=[],
- tool_event_names=[],
- event_sources=[],
- tool_statuses=[],
- tool_latencies_ms=[],
- semantic_failures=[],
- error=error,
- model_calls=[],
- )
- class FakeRunner:
- def __init__(self, results: list[BenchmarkRunResult]) -> None:
- self.results = results
- async def run(self) -> list[BenchmarkRunResult]:
- return self.results
- def _dependencies(
- cli,
- *,
- results: list[BenchmarkRunResult],
- api_key: str = "",
- runner_calls: list[dict[str, object]] | None = None,
- report_writer=None,
- stderr: StringIO | None = None,
- stdout: StringIO | None = None,
- ):
- calls = runner_calls if runner_calls is not None else []
- def runner_factory(**kwargs):
- calls.append(kwargs)
- return FakeRunner(results)
- return cli.BenchmarkCliDependencies(
- settings_factory=lambda: SimpleNamespace(
- openai_api_key=api_key,
- openai_base_url="https://env-must-not-win.example/v1",
- openai_default_model="env-must-not-win",
- ),
- runner_factory=runner_factory,
- report_writer=report_writer or cli.write_benchmark_report,
- now=lambda: datetime(2026, 7, 14, 1, 2, 3, tzinfo=timezone.utc),
- stdout=stdout or StringIO(),
- stderr=stderr or StringIO(),
- )
- def test_cli_overrides_json_and_repeatable_case_mode_replace_lists(tmp_path: Path):
- cli = _cli_module()
- config_path = _write_config(
- tmp_path / "config.json",
- cases=["ordinary_chat", "web_search_two_answers"],
- modes=["dual_agent", "chat_agent_tools"],
- )
- output_dir = tmp_path / "reports"
- runner_calls: list[dict[str, object]] = []
- dependencies = _dependencies(
- cli,
- results=[
- _run_result(
- case_id=BenchmarkCaseId.SESSION_TERMINATE,
- mode=BenchmarkMode.CHAT_AGENT_TOOLS,
- )
- ],
- api_key="env-key",
- runner_calls=runner_calls,
- )
- exit_code = cli.run_cli(
- [
- "--config",
- str(config_path),
- "--base-url",
- "https://cli.example/v1",
- "--model",
- "cli-model",
- "--runs",
- "3",
- "--case",
- "ordinary_chat",
- "--case",
- "session_terminate",
- "--mode",
- "chat_agent_tools",
- "--output-dir",
- str(output_dir),
- ],
- dependencies=dependencies,
- )
- assert exit_code == 0
- assert len(runner_calls) == 1
- resolved = runner_calls[0]
- assert resolved["api_key"] == "env-key"
- assert resolved["mock"] is False
- config = resolved["config"]
- assert config.base_url == "https://cli.example/v1"
- assert config.model == "cli-model"
- assert config.runs_per_case == 3
- assert config.cases == [
- BenchmarkCaseId.ORDINARY_CHAT,
- BenchmarkCaseId.SESSION_TERMINATE,
- ]
- assert config.modes == [BenchmarkMode.CHAT_AGENT_TOOLS]
- assert len(list(output_dir.glob("*.json"))) == 1
- assert len(list(output_dir.glob("*.md"))) == 1
- def test_mock_run_needs_no_api_key_and_exits_zero(tmp_path: Path):
- cli = _cli_module()
- config_path = _write_config(tmp_path / "config.json")
- output_dir = tmp_path / "reports"
- stdout = StringIO()
- stderr = StringIO()
- dependencies = _dependencies(
- cli,
- results=[_run_result()],
- api_key="",
- stdout=stdout,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- [
- "--config",
- str(config_path),
- "--mock",
- "--output-dir",
- str(output_dir),
- ],
- dependencies=dependencies,
- )
- assert exit_code == 0
- assert "attempted=1 passed=1 failed=0" in stdout.getvalue()
- assert "json_report=" in stdout.getvalue()
- assert "markdown_report=" in stdout.getvalue()
- assert stderr.getvalue() == ""
- def test_partial_failed_run_exits_one_writes_redacted_reports(tmp_path: Path):
- cli = _cli_module()
- secret = "sk-cli-real-secret"
- config_path = _write_config(tmp_path / "config.json")
- output_dir = tmp_path / "reports"
- stdout = StringIO()
- stderr = StringIO()
- dependencies = _dependencies(
- cli,
- results=[
- _run_result(iteration=1),
- _run_result(
- iteration=2,
- status="failed",
- error=f"Authorization: Bearer {secret}",
- ),
- ],
- api_key=secret,
- stdout=stdout,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- [
- "--config",
- str(config_path),
- "--output-dir",
- str(output_dir),
- ],
- dependencies=dependencies,
- )
- assert exit_code == 1
- assert "attempted=2 passed=1 failed=1" in stdout.getvalue()
- assert secret not in stdout.getvalue()
- assert stderr.getvalue() == ""
- report_text = "\n".join(
- path.read_text() for path in sorted(output_dir.iterdir())
- )
- assert secret not in report_text
- assert "[REDACTED]" in report_text
- @pytest.mark.parametrize(
- "payload",
- [
- {"schema_version": 1, "api_key": "secret-in-json"},
- {"schema_version": 1, "base_url": "not-a-url", "model": "m"},
- ],
- )
- def test_bad_config_exits_two_without_leaking_secret(
- tmp_path: Path, payload: dict[str, object]
- ):
- cli = _cli_module()
- config_path = tmp_path / "bad.json"
- config_path.write_text(json.dumps(payload))
- stderr = StringIO()
- runner_calls: list[dict[str, object]] = []
- dependencies = _dependencies(
- cli,
- results=[],
- runner_calls=runner_calls,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- ["--config", str(config_path), "--mock"],
- dependencies=dependencies,
- )
- assert exit_code == 2
- assert runner_calls == []
- assert "secret-in-json" not in stderr.getvalue()
- assert "benchmark error:" in stderr.getvalue()
- def test_final_validation_allows_cli_to_supply_missing_base_url_and_model(
- tmp_path: Path,
- ):
- cli = _cli_module()
- config_path = tmp_path / "minimal.json"
- config_path.write_text(
- json.dumps(
- {
- "schema_version": 1,
- "cases": ["ordinary_chat"],
- "modes": ["dual_agent"],
- }
- )
- )
- runner_calls: list[dict[str, object]] = []
- dependencies = _dependencies(
- cli,
- results=[_run_result()],
- runner_calls=runner_calls,
- )
- exit_code = cli.run_cli(
- [
- "--config",
- str(config_path),
- "--base-url",
- "https://cli.example/v1",
- "--model",
- "cli-model",
- "--mock",
- "--output-dir",
- str(tmp_path / "reports"),
- ],
- dependencies=dependencies,
- )
- assert exit_code == 0
- assert runner_calls[0]["config"].model == "cli-model"
- def test_live_run_without_api_key_exits_two(tmp_path: Path):
- cli = _cli_module()
- config_path = _write_config(tmp_path / "config.json")
- stderr = StringIO()
- runner_calls: list[dict[str, object]] = []
- dependencies = _dependencies(
- cli,
- results=[],
- api_key="",
- runner_calls=runner_calls,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- ["--config", str(config_path)],
- dependencies=dependencies,
- )
- assert exit_code == 2
- assert runner_calls == []
- assert "api key is required" in stderr.getvalue().lower()
- def test_report_failure_exits_two_and_redacts_stderr(tmp_path: Path):
- cli = _cli_module()
- secret = "sk-report-failure-secret"
- config_path = _write_config(tmp_path / "config.json")
- stderr = StringIO()
- def failing_writer(report, output_dir):
- del report, output_dir
- raise OSError(f"API-Key: {secret}")
- dependencies = _dependencies(
- cli,
- results=[_run_result()],
- api_key=secret,
- report_writer=failing_writer,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- ["--config", str(config_path)],
- dependencies=dependencies,
- )
- assert exit_code == 2
- assert secret not in stderr.getvalue()
- assert "[REDACTED]" in stderr.getvalue()
- def test_unexpected_runner_failure_still_attempts_report_write(tmp_path: Path):
- cli = _cli_module()
- secret = "runner-secret"
- config_path = _write_config(tmp_path / "config.json")
- writes: list[object] = []
- stderr = StringIO()
- stdout = StringIO()
- class FailingRunner:
- async def run(self):
- raise RuntimeError(f"Bearer {secret}")
- def write_report(report, output_dir):
- writes.append(report)
- output = Path(output_dir)
- output.mkdir(parents=True)
- json_path = output / "report.json"
- md_path = output / "report.md"
- json_path.write_text("{}")
- md_path.write_text("# report")
- return json_path, md_path
- dependencies = cli.BenchmarkCliDependencies(
- settings_factory=lambda: SimpleNamespace(openai_api_key=secret),
- runner_factory=lambda **kwargs: FailingRunner(),
- report_writer=write_report,
- now=lambda: datetime(2026, 7, 14, tzinfo=timezone.utc),
- stdout=stdout,
- stderr=stderr,
- )
- exit_code = cli.run_cli(
- [
- "--config",
- str(config_path),
- "--output-dir",
- str(tmp_path / "reports"),
- ],
- dependencies=dependencies,
- )
- assert exit_code == 2
- assert len(writes) == 1
- assert writes[0].overall.attempted == 1
- assert writes[0].overall.failed == 1
- assert writes[0].overall.system_errors == ["Bearer [REDACTED]"]
- assert "attempted=1 passed=0 failed=1" in stdout.getvalue()
- assert secret not in stdout.getvalue()
- assert secret not in stderr.getvalue()
- assert "Bearer [REDACTED]" in stderr.getvalue()
- def test_help_and_required_config_use_argparse(capsys: pytest.CaptureFixture[str]):
- cli = _cli_module()
- with pytest.raises(SystemExit) as help_exit:
- cli.run_cli(["--help"])
- assert help_exit.value.code == 0
- assert "--config" in capsys.readouterr().out
- with pytest.raises(SystemExit) as missing_exit:
- cli.run_cli([])
- assert missing_exit.value.code == 2
- def test_argparse_errors_are_redacted(capsys: pytest.CaptureFixture[str]):
- cli = _cli_module()
- secret = "argument-secret"
- with pytest.raises(SystemExit) as exc_info:
- cli.run_cli(
- [
- "--config",
- "unused.json",
- "--mode",
- f"api_key={secret}",
- ]
- )
- assert exc_info.value.code == 2
- stderr = capsys.readouterr().err
- assert secret not in stderr
- assert "[REDACTED]" in stderr
- def test_main_raises_system_exit(monkeypatch: pytest.MonkeyPatch):
- cli = _cli_module()
- monkeypatch.setattr(cli, "run_cli", lambda argv=None: 1)
- with pytest.raises(SystemExit) as exc_info:
- cli.main()
- assert exc_info.value.code == 1
- def test_pyproject_registers_benchmark_console_script():
- pyproject = tomllib.loads(Path("pyproject.toml").read_text())
- assert pyproject["project"]["scripts"]["agent-lab-benchmark"] == (
- "agent_lab.presentation.benchmark_cli:main"
- )
|