blob: 8bac034debb59cc77168652fce3a2323aab1985a [file]
# 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