| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| from __future__ import annotations |
| |
| import importlib.util |
| from pathlib import Path |
| from types import SimpleNamespace |
| |
| import pytest |
| |
| REPO_ROOT = Path(__file__).resolve().parents[4] |
| MODULE_PATH = REPO_ROOT / "dev" / "skill-evals" / "eval.py" |
| |
| |
| @pytest.fixture(scope="module") |
| def skill_eval_module(): |
| spec = importlib.util.spec_from_file_location("skill_eval", MODULE_PATH) |
| assert spec and spec.loader |
| module = importlib.util.module_from_spec(spec) |
| spec.loader.exec_module(module) |
| return module |
| |
| |
| @pytest.mark.parametrize( |
| ("configured", "expected"), |
| [ |
| pytest.param(None, "claude", id="default"), |
| pytest.param("Claude", "claude", id="case-insensitive"), |
| pytest.param("codex", "codex", id="codex"), |
| ], |
| ) |
| def test_get_runtime(monkeypatch, skill_eval_module, configured, expected): |
| if configured is None: |
| monkeypatch.delenv("AGENT_RUNTIME", raising=False) |
| else: |
| monkeypatch.setenv("AGENT_RUNTIME", configured) |
| |
| assert skill_eval_module.get_runtime() == expected |
| |
| |
| def test_get_runtime_rejects_unknown_runtime(monkeypatch, skill_eval_module): |
| monkeypatch.setenv("AGENT_RUNTIME", "unknown") |
| |
| with pytest.raises(ValueError, match="claude, codex"): |
| skill_eval_module.get_runtime() |
| |
| |
| def test_build_codex_provider(skill_eval_module, tmp_path): |
| working_dir = tmp_path / "worktree" |
| |
| provider = skill_eval_module.build_codex_provider( |
| "working", working_dir, "gpt-test", detect_skill_usage=True |
| ) |
| |
| assert provider == { |
| "id": "openai:codex-sdk", |
| "label": "working", |
| "config": { |
| "approval_policy": "never", |
| "cli_config": { |
| "history": {"persistence": "none"}, |
| "project_doc_max_bytes": skill_eval_module.CODEX_PROJECT_DOC_MAX_BYTES, |
| }, |
| "enable_streaming": True, |
| "model": "gpt-test", |
| "network_access_enabled": False, |
| "output_schema": skill_eval_module.OUTPUT_FORMAT["schema"], |
| "sandbox_mode": "read-only", |
| "web_search_mode": "disabled", |
| "working_dir": str(working_dir), |
| }, |
| "transform": "JSON.parse(output)", |
| } |
| |
| |
| def test_output_schema_requires_every_property(skill_eval_module): |
| schema = skill_eval_module.OUTPUT_FORMAT["schema"] |
| |
| assert set(schema["required"]) == set(schema["properties"]) |
| |
| |
| def test_build_codex_provider_uses_runtime_default_model(skill_eval_module, tmp_path): |
| provider = skill_eval_module.build_codex_provider("main", tmp_path / "worktree", None) |
| |
| assert "model" not in provider["config"] |
| assert "enable_streaming" not in provider["config"] |
| |
| |
| @pytest.fixture |
| def promptfoo_layout(monkeypatch, skill_eval_module, tmp_path): |
| """Build a prek-style node env and return an SDK installer for either location. |
| |
| ``install_root`` mirrors a package listed in the hook's |
| additional_dependencies; ``bundled`` mirrors promptfoo's own optional |
| dependency, nested inside its package. |
| """ |
| install_root = tmp_path / "node_modules" |
| promptfoo = install_root / "promptfoo" |
| bundled = promptfoo / "node_modules" |
| promptfoo.mkdir(parents=True) |
| (promptfoo / "package.json").write_text("{}") |
| promptfoo_bin = promptfoo / "promptfoo" |
| promptfoo_bin.touch() |
| monkeypatch.setattr(skill_eval_module.shutil, "which", lambda _: str(promptfoo_bin)) |
| monkeypatch.setattr( |
| skill_eval_module, |
| "run", |
| lambda *args, **kwargs: SimpleNamespace(stdout=f"{skill_eval_module.PROMPTFOO_VERSION}\n"), |
| ) |
| |
| def install(location: Path, package: str) -> None: |
| sdk = location.joinpath(*package.split("/")) |
| sdk.mkdir(parents=True) |
| (sdk / "package.json").write_text('{"version": "1.2.3"}') |
| |
| return SimpleNamespace(install_root=install_root, bundled=bundled, install=install) |
| |
| |
| @pytest.mark.parametrize("runtime", ["claude", "codex"]) |
| def test_find_sdk_modules_resolves_the_hook_installed_copy(skill_eval_module, promptfoo_layout, runtime): |
| promptfoo_layout.install(promptfoo_layout.install_root, skill_eval_module.SDK_PACKAGES[runtime]) |
| |
| assert skill_eval_module.find_sdk_modules(runtime) == promptfoo_layout.install_root |
| |
| |
| @pytest.mark.parametrize("runtime", ["claude", "codex"]) |
| def test_find_sdk_modules_ignores_the_copy_bundled_with_promptfoo( |
| skill_eval_module, promptfoo_layout, runtime |
| ): |
| promptfoo_layout.install(promptfoo_layout.bundled, skill_eval_module.SDK_PACKAGES[runtime]) |
| |
| with pytest.raises(SystemExit) as exit_info: |
| skill_eval_module.find_sdk_modules(runtime) |
| |
| assert exit_info.value.code == 1 |
| |
| |
| def test_find_sdk_modules_requires_the_selected_runtimes_sdk(skill_eval_module, promptfoo_layout): |
| promptfoo_layout.install(promptfoo_layout.install_root, skill_eval_module.SDK_PACKAGES["claude"]) |
| |
| with pytest.raises(SystemExit) as exit_info: |
| skill_eval_module.find_sdk_modules("codex") |
| |
| assert exit_info.value.code == 1 |