metrik-bench 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,89 @@
1
+ # Metrik — .gitignore
2
+ #
3
+ # NOTE: the previous version of this file contained `docs/*`, `.gitignore`, and `.claude`,
4
+ # which excluded the entire documentation tree — i.e. every Phase 0/1 deliverable — from
5
+ # version control. Replaced deliberately. docs/ is the project's primary output right now.
6
+
7
+ # --- Build output that must never be committed ---
8
+ # ADR 016: diagram sources live in docs/diagrams/*.mmd; rendered images are build output.
9
+ docs/static/diagrams/
10
+ # ADR 015: generated reference docs are committed on purpose (drift detection), so they
11
+ # are NOT ignored here — see docs/reference/.
12
+
13
+ # --- Python ---
14
+ __pycache__/
15
+ *.py[cod]
16
+ *.so
17
+ .Python
18
+ build/
19
+ dist/
20
+ *.egg-info/
21
+ .eggs/
22
+ .venv/
23
+ venv/
24
+ env/
25
+
26
+ # Tooling caches
27
+ .pytest_cache/
28
+ .mypy_cache/
29
+ .ruff_cache/
30
+ .hypothesis/
31
+ .coverage
32
+ .coverage.*
33
+ htmlcov/
34
+ coverage.xml
35
+ .benchmarks/
36
+ .tox/
37
+ .nox/
38
+
39
+ # uv (ADR 013) — uv.lock IS committed; the cache is not.
40
+ .uv/
41
+
42
+ # --- Node / web (ADR 009) ---
43
+ node_modules/
44
+ .next/
45
+ out/
46
+ .turbo/
47
+ *.tsbuildinfo
48
+ .pnpm-store/
49
+
50
+ # --- Tauri (ADR 010) ---
51
+ apps/desktop/src-tauri/target/
52
+ apps/desktop/src-tauri/gen/
53
+
54
+ # --- Docs site (ADR 015) ---
55
+ docs/.docusaurus/
56
+ docs/build/
57
+
58
+ # --- Metrik runtime data ---
59
+ # The artifact store and blob store (ADR 006). Content-addressed, machine-generated,
60
+ # gigabytes. Never in git.
61
+ .metrik/
62
+ *.metrik-store/
63
+ # Local config may hold API keys (ADR 018).
64
+ metrik.local.toml
65
+ .env
66
+ .env.*
67
+ !.env.example
68
+
69
+ # Model weights and checkpoints — always too large, often license-encumbered (plan.md §5.7).
70
+ *.safetensors
71
+ *.gguf
72
+ *.onnx
73
+ *.pt
74
+ *.pth
75
+ *.bin
76
+ # ...except the tiny-random fixtures, which are committed on purpose (ADR 017).
77
+ !tests/fixtures/**
78
+
79
+ # --- Editors and OS ---
80
+ .vscode/
81
+ !.vscode/extensions.json
82
+ .idea/
83
+ *.swp
84
+ .DS_Store
85
+ Thumbs.db
86
+ desktop.ini
87
+
88
+ # --- Local agent/tooling settings ---
89
+ .claude/settings.local.json
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: metrik-bench
3
+ Version: 0.1.0
4
+ Summary: Metrik Benchmark Lab — sandboxed evaluation, or a refusal. Never an unsandboxed run.
5
+ Project-URL: Homepage, https://github.com/Asmodeus14/Metrik
6
+ Project-URL: Repository, https://github.com/Asmodeus14/Metrik
7
+ Project-URL: Issues, https://github.com/Asmodeus14/Metrik/issues
8
+ Author: The Metrik Authors
9
+ License-Expression: Apache-2.0
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: metrik-sdk==0.1.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # metrik-bench
18
+
19
+ Sandboxed evaluation — or a refusal. Never an unsandboxed run.
20
+
21
+ ```console
22
+ $ metrik bench --check # what isolation is available here, and what is not
23
+ $ metrik bench <model> --task humaneval
24
+ ```
25
+
26
+ ## Why this package is unusual
27
+
28
+ HumanEval and MBPP ask a model to write code and then **run it**. A benchmark runner that
29
+ `exec()`s model output on your machine is a remote-code-execution primitive with extra steps.
30
+
31
+ [ADR 019](../../docs/planning/adr/019-benchmark-sandbox.md) answers that by refusing rather
32
+ than degrading, and records the clause as **not revisitable**. There is no in-process fallback,
33
+ no `--i-know-what-im-doing` flag, and no configuration that produces one.
34
+
35
+ That is enforced in two places, deliberately:
36
+
37
+ - **The type.** `BenchResult` cannot be constructed for a code-executing task without a sandbox
38
+ attestation, and `isolation: "none"` fails validation. A runner that finds no container
39
+ runtime can emit nothing at all.
40
+ - **The runner.** It checks for a runtime *before* generating anything, so the refusal arrives
41
+ in a second rather than after an hour of generation.
42
+
43
+ ## What a refusal looks like
44
+
45
+ ```
46
+ error: humaneval executes model-generated code and no container runtime is available
47
+ -> Install Docker Desktop and start it, then re-run. `metrik bench --check` shows
48
+ what was probed. Tasks that do not execute code (mmlu) run without a container.
49
+ ```
50
+
51
+ Note the last sentence: MMLU is multiple choice and needs no container. Requiring one for
52
+ everything would push people to disable the check they actually need.
@@ -0,0 +1,36 @@
1
+ # metrik-bench
2
+
3
+ Sandboxed evaluation — or a refusal. Never an unsandboxed run.
4
+
5
+ ```console
6
+ $ metrik bench --check # what isolation is available here, and what is not
7
+ $ metrik bench <model> --task humaneval
8
+ ```
9
+
10
+ ## Why this package is unusual
11
+
12
+ HumanEval and MBPP ask a model to write code and then **run it**. A benchmark runner that
13
+ `exec()`s model output on your machine is a remote-code-execution primitive with extra steps.
14
+
15
+ [ADR 019](../../docs/planning/adr/019-benchmark-sandbox.md) answers that by refusing rather
16
+ than degrading, and records the clause as **not revisitable**. There is no in-process fallback,
17
+ no `--i-know-what-im-doing` flag, and no configuration that produces one.
18
+
19
+ That is enforced in two places, deliberately:
20
+
21
+ - **The type.** `BenchResult` cannot be constructed for a code-executing task without a sandbox
22
+ attestation, and `isolation: "none"` fails validation. A runner that finds no container
23
+ runtime can emit nothing at all.
24
+ - **The runner.** It checks for a runtime *before* generating anything, so the refusal arrives
25
+ in a second rather than after an hour of generation.
26
+
27
+ ## What a refusal looks like
28
+
29
+ ```
30
+ error: humaneval executes model-generated code and no container runtime is available
31
+ -> Install Docker Desktop and start it, then re-run. `metrik bench --check` shows
32
+ what was probed. Tasks that do not execute code (mmlu) run without a container.
33
+ ```
34
+
35
+ Note the last sentence: MMLU is multiple choice and needs no container. Requiring one for
36
+ everything would push people to disable the check they actually need.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "metrik-bench"
7
+ version = "0.1.0"
8
+ description = "Metrik Benchmark Lab — sandboxed evaluation, or a refusal. Never an unsandboxed run."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "The Metrik Authors" }]
13
+ classifiers = [
14
+ "Development Status :: 2 - Pre-Alpha",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
17
+ ]
18
+
19
+ # No harness dependency yet. `lm-eval-harness` arrives pinned (ADR 022) with the first task
20
+ # that needs it; this package currently owns runtime detection and the refusal path, which
21
+ # deliberately need nothing but the standard library.
22
+ dependencies = [
23
+ "metrik-sdk==0.1.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Asmodeus14/Metrik"
28
+ Repository = "https://github.com/Asmodeus14/Metrik"
29
+ Issues = "https://github.com/Asmodeus14/Metrik/issues"
30
+
31
+ [tool.uv.sources]
32
+ metrik-sdk = { workspace = true }
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/metrik"]
@@ -0,0 +1,29 @@
1
+ """Metrik Benchmark Lab: sandboxed evaluation, or a refusal.
2
+
3
+ The runner is not implemented yet, and the order is deliberate -- the gate ships before the
4
+ thing it gates (ADR 019). What exists is runtime detection, the task catalogue, and the
5
+ refusal path, so that a machine which cannot contain model-generated code establishes that
6
+ before anything is generated rather than after.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from metrik.bench.runtime import (
12
+ CANDIDATES,
13
+ Availability,
14
+ RuntimeInfo,
15
+ detect_runtimes,
16
+ require_runtime,
17
+ )
18
+ from metrik.bench.tasks import TASKS, TaskSpec, resolve_task
19
+
20
+ __all__ = [
21
+ "CANDIDATES",
22
+ "TASKS",
23
+ "Availability",
24
+ "RuntimeInfo",
25
+ "TaskSpec",
26
+ "detect_runtimes",
27
+ "require_runtime",
28
+ "resolve_task",
29
+ ]
File without changes
@@ -0,0 +1,169 @@
1
+ """Finding a container runtime, or establishing that there is not one.
2
+
3
+ ``artifacts.md`` §6.12, ADR 019. The one job here is to answer *"can this machine contain
4
+ model-generated code?"* honestly, before anything is generated.
5
+
6
+ Two distinctions this makes that a naive check does not.
7
+
8
+ **Installed is not running.** ``docker --version`` answers from the CLI binary alone and
9
+ succeeds happily while Docker Desktop is stopped. The question that matters is whether a
10
+ container can actually be started, which only ``docker info`` answers -- it talks to the
11
+ daemon. Treating the binary as proof is how a runner concludes it is safe and then discovers
12
+ otherwise at the first sample.
13
+
14
+ **Absent is not broken.** "No runtime installed" and "runtime installed but the daemon is
15
+ down" need different instructions, so they are different results rather than one boolean.
16
+
17
+ Probing is a capability check, never a hardcoded binary name: ``SandboxAttestation.runtime``
18
+ records whichever was used, so adding Podman later is an entry in :data:`CANDIDATES` rather
19
+ than a change to anything that consumes the result.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import shutil
25
+ import subprocess
26
+ from dataclasses import dataclass
27
+ from enum import StrEnum
28
+ from typing import Final
29
+
30
+ from metrik.sdk import UnsupportedError
31
+
32
+ __all__ = [
33
+ "CANDIDATES",
34
+ "PROBE_TIMEOUT_S",
35
+ "Availability",
36
+ "RuntimeInfo",
37
+ "detect_runtimes",
38
+ "require_runtime",
39
+ ]
40
+
41
+ #: Runtimes probed, in preference order. Docker first per ``plan.md`` §13 decision 4 -- it is
42
+ #: what most contributors already have. Podman is probed anyway because it costs one
43
+ #: subprocess and a machine that has it is a machine that can run benchmarks.
44
+ CANDIDATES: Final[tuple[str, ...]] = ("docker", "podman")
45
+
46
+ #: A stopped Docker Desktop can leave `docker info` hanging on a named pipe. A benchmark run
47
+ #: waiting indefinitely to find out whether it may start is worse than a fast refusal.
48
+ PROBE_TIMEOUT_S: Final = 10.0
49
+
50
+
51
+ class Availability(StrEnum):
52
+ """Why a runtime can or cannot be used."""
53
+
54
+ READY = "ready"
55
+ #: Binary present, daemon unreachable. The common Docker Desktop case, and the one where
56
+ #: telling someone to "install Docker" wastes their time.
57
+ NOT_RUNNING = "not_running"
58
+ NOT_INSTALLED = "not_installed"
59
+ #: Present and responding, but the probe itself failed -- a permissions problem, usually.
60
+ UNUSABLE = "unusable"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class RuntimeInfo:
65
+ """What one candidate runtime looks like on this machine."""
66
+
67
+ name: str
68
+ availability: Availability
69
+ version: str | None = None
70
+ detail: str = ""
71
+
72
+ @property
73
+ def ready(self) -> bool:
74
+ return self.availability is Availability.READY
75
+
76
+ @property
77
+ def remediation(self) -> str:
78
+ """What this specific machine needs, rather than generic advice."""
79
+ if self.availability is Availability.NOT_RUNNING:
80
+ return f"{self.name} is installed but not responding. Start it and re-run."
81
+ if self.availability is Availability.NOT_INSTALLED:
82
+ return f"Install {self.name}."
83
+ if self.availability is Availability.UNUSABLE:
84
+ return f"{self.name} responded with an error: {self.detail}"
85
+ return ""
86
+
87
+
88
+ def _run(command: list[str]) -> tuple[int, str]:
89
+ """Run a probe, tolerating every way it can fail to answer."""
90
+ try:
91
+ completed = subprocess.run(
92
+ command,
93
+ capture_output=True,
94
+ text=True,
95
+ timeout=PROBE_TIMEOUT_S,
96
+ check=False,
97
+ )
98
+ except subprocess.TimeoutExpired:
99
+ return 124, "timed out"
100
+ except OSError as exc:
101
+ return 127, str(exc)
102
+ output = (completed.stdout or completed.stderr or "").strip()
103
+ return completed.returncode, output.splitlines()[0] if output else ""
104
+
105
+
106
+ def _probe(name: str) -> RuntimeInfo:
107
+ if shutil.which(name) is None:
108
+ return RuntimeInfo(name, Availability.NOT_INSTALLED, detail="not on PATH")
109
+
110
+ # `info`, not `--version`. The version comes from the CLI binary and says nothing about
111
+ # whether a container can be started; `info` requires the daemon to answer.
112
+ code, output = _run([name, "info", "--format", "{{.ServerVersion}}"])
113
+ if code == 0 and output:
114
+ return RuntimeInfo(name, Availability.READY, version=output)
115
+ if code == 124:
116
+ return RuntimeInfo(
117
+ name,
118
+ Availability.NOT_RUNNING,
119
+ detail=f"`{name} info` timed out after {PROBE_TIMEOUT_S:g}s",
120
+ )
121
+ # A non-zero `info` with the binary present almost always means the daemon is down; the
122
+ # message is kept so an unusual cause is still visible rather than flattened.
123
+ return RuntimeInfo(name, Availability.NOT_RUNNING, detail=output or f"`{name} info` failed")
124
+
125
+
126
+ def detect_runtimes() -> list[RuntimeInfo]:
127
+ """Probe every candidate and report all of them.
128
+
129
+ All of them, not the first that works: ``metrik bench --check`` has to be able to say what
130
+ was looked for and what each one said, or "no runtime available" is unactionable.
131
+ """
132
+ return [_probe(name) for name in CANDIDATES]
133
+
134
+
135
+ def require_runtime(*, task_id: str) -> RuntimeInfo:
136
+ """Return a usable runtime, or refuse.
137
+
138
+ The refusal is the feature. ADR 019 permits no in-process fallback and no flag that enables
139
+ one, so this raises and the caller stops -- there is deliberately no second return value
140
+ meaning "carry on unsafely".
141
+
142
+ Called *before* generation, so a machine that cannot contain the run finds out in a second
143
+ rather than after an hour of decoding.
144
+ """
145
+ found = detect_runtimes()
146
+ for runtime in found:
147
+ if runtime.ready:
148
+ return runtime
149
+
150
+ installed = [r for r in found if r.availability is not Availability.NOT_INSTALLED]
151
+ if installed:
152
+ # Something is there but not answering. Say which, and what to do about that one --
153
+ # "install Docker" to someone who has Docker installed reads as a broken tool.
154
+ lines = "; ".join(f"{r.name}: {r.detail}" for r in installed)
155
+ remediation = " ".join(r.remediation for r in installed)
156
+ else:
157
+ lines = "none of " + ", ".join(CANDIDATES) + " is installed"
158
+ remediation = (
159
+ "Install Docker Desktop and start it (plan.md sec. 13 decision 4), or Podman. "
160
+ )
161
+
162
+ raise UnsupportedError(
163
+ f"{task_id} executes model-generated code and no container runtime is available ({lines})",
164
+ remediation=(
165
+ f"{remediation}Run `metrik bench --check` to see what was probed. Tasks that do "
166
+ f"not execute code still run without a container. There is no unsandboxed mode "
167
+ f"(ADR 019)."
168
+ ),
169
+ )
@@ -0,0 +1,72 @@
1
+ """The task catalogue: which benchmarks exist, and which of them run code.
2
+
3
+ ``executes_code`` is declared per task and never inferred from the id. A pattern match on
4
+ names like ``*eval*`` would be wrong in both directions -- MMLU contains "eval" in plenty of
5
+ spellings and does not execute anything, while a new agentic benchmark might execute
6
+ everything and be called something else entirely. The failure that matters is a
7
+ code-executing task defaulting to "safe", so nothing here guesses.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Final
14
+
15
+ from metrik.sdk import UserError
16
+
17
+ __all__ = ["TASKS", "TaskSpec", "resolve_task"]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class TaskSpec:
22
+ """A benchmark this build knows how to run."""
23
+
24
+ id: str
25
+ version: str
26
+ #: The field that gates sandboxing (``artifacts.md`` §6.12). Declared, never inferred.
27
+ executes_code: bool
28
+ summary: str
29
+ n_items: int | None = None
30
+ harness: str = "lm_eval_harness"
31
+
32
+
33
+ TASKS: Final[dict[str, TaskSpec]] = {
34
+ "humaneval": TaskSpec(
35
+ id="humaneval",
36
+ version="1.0",
37
+ executes_code=True,
38
+ summary="164 Python problems; generated solutions are executed against unit tests.",
39
+ n_items=164,
40
+ harness="metrik_code_exec",
41
+ ),
42
+ "mbpp": TaskSpec(
43
+ id="mbpp",
44
+ version="1.0",
45
+ executes_code=True,
46
+ summary="Mostly Basic Python Problems; generated solutions are executed.",
47
+ n_items=500,
48
+ harness="metrik_code_exec",
49
+ ),
50
+ "mmlu": TaskSpec(
51
+ id="mmlu",
52
+ version="1.0",
53
+ executes_code=False,
54
+ summary="Multiple choice across 57 subjects. Nothing is executed.",
55
+ n_items=14042,
56
+ ),
57
+ }
58
+
59
+
60
+ def resolve_task(task_id: str) -> TaskSpec:
61
+ """Look up a task, or fail with the list of what exists.
62
+
63
+ An unknown task is refused rather than treated as a custom one. Accepting it would mean
64
+ guessing ``executes_code``, and the safe-looking guess is the dangerous one.
65
+ """
66
+ spec = TASKS.get(task_id.lower())
67
+ if spec is None:
68
+ raise UserError(
69
+ f"unknown task {task_id!r}",
70
+ remediation=f"Known tasks: {', '.join(sorted(TASKS))}. Run `metrik bench --check`.",
71
+ )
72
+ return spec
@@ -0,0 +1,140 @@
1
+ """Runtime detection and the refusal path.
2
+
3
+ The property under test is that there is **no way through** without a runtime. ADR 019 permits
4
+ no in-process fallback and no flag enabling one, so `require_runtime` either returns something
5
+ usable or raises -- there is deliberately no third outcome.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ import subprocess
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from metrik.bench import Availability, RuntimeInfo, detect_runtimes, require_runtime, resolve_task
17
+ from metrik.bench import runtime as runtime_module
18
+ from metrik.sdk import UnsupportedError, UserError
19
+
20
+ pytestmark = pytest.mark.unit
21
+
22
+
23
+ def _fake_probe(monkeypatch: pytest.MonkeyPatch, results: dict[str, RuntimeInfo]) -> None:
24
+ monkeypatch.setattr(
25
+ runtime_module,
26
+ "_probe",
27
+ lambda name: results.get(name, RuntimeInfo(name, Availability.NOT_INSTALLED)),
28
+ )
29
+
30
+
31
+ # -- the refusal -------------------------------------------------------------------------------
32
+
33
+
34
+ def test_no_runtime_means_refusal(monkeypatch: pytest.MonkeyPatch) -> None:
35
+ _fake_probe(monkeypatch, {})
36
+ with pytest.raises(UnsupportedError, match="no container runtime is available"):
37
+ require_runtime(task_id="humaneval")
38
+
39
+
40
+ def test_the_refusal_says_there_is_no_unsandboxed_mode(monkeypatch: pytest.MonkeyPatch) -> None:
41
+ """The message has to close the door, or the next question is "how do I skip it"."""
42
+ _fake_probe(monkeypatch, {})
43
+ with pytest.raises(UnsupportedError) as caught:
44
+ require_runtime(task_id="humaneval")
45
+ assert "no unsandboxed mode" in (caught.value.remediation or "")
46
+
47
+
48
+ def test_an_installed_but_stopped_runtime_gets_different_advice(
49
+ monkeypatch: pytest.MonkeyPatch,
50
+ ) -> None:
51
+ """Telling someone with Docker installed to "install Docker" reads as a broken tool."""
52
+ _fake_probe(
53
+ monkeypatch,
54
+ {"docker": RuntimeInfo("docker", Availability.NOT_RUNNING, detail="daemon unreachable")},
55
+ )
56
+ with pytest.raises(UnsupportedError) as caught:
57
+ require_runtime(task_id="humaneval")
58
+ remediation = caught.value.remediation or ""
59
+ assert "not responding" in remediation
60
+ assert "Install docker." not in remediation
61
+
62
+
63
+ def test_a_ready_runtime_is_returned(monkeypatch: pytest.MonkeyPatch) -> None:
64
+ _fake_probe(monkeypatch, {"docker": RuntimeInfo("docker", Availability.READY, version="27.0")})
65
+ assert require_runtime(task_id="humaneval").name == "docker"
66
+
67
+
68
+ def test_the_second_candidate_is_used_when_the_first_is_down(
69
+ monkeypatch: pytest.MonkeyPatch,
70
+ ) -> None:
71
+ _fake_probe(
72
+ monkeypatch,
73
+ {
74
+ "docker": RuntimeInfo("docker", Availability.NOT_RUNNING),
75
+ "podman": RuntimeInfo("podman", Availability.READY, version="5.1"),
76
+ },
77
+ )
78
+ assert require_runtime(task_id="humaneval").name == "podman"
79
+
80
+
81
+ # -- installed is not running --------------------------------------------------------------------
82
+
83
+
84
+ def test_a_present_binary_with_a_dead_daemon_is_not_ready(monkeypatch: pytest.MonkeyPatch) -> None:
85
+ """`docker --version` succeeds with Docker Desktop stopped. Only `info` reaches the daemon,
86
+ and only the daemon can start a container."""
87
+ monkeypatch.setattr(shutil, "which", lambda _n: "/usr/bin/docker")
88
+ monkeypatch.setattr(
89
+ runtime_module, "_run", lambda _c: (1, "Cannot connect to the Docker daemon")
90
+ )
91
+ probed = runtime_module._probe("docker")
92
+ assert probed.availability is Availability.NOT_RUNNING
93
+ assert not probed.ready
94
+
95
+
96
+ def test_a_hanging_probe_becomes_a_refusal_not_a_hang(monkeypatch: pytest.MonkeyPatch) -> None:
97
+ """A stopped Docker Desktop can leave `info` blocked on a named pipe. Waiting forever to
98
+ find out whether a run may start is worse than refusing quickly."""
99
+
100
+ def _timeout(*_a: Any, **_k: Any) -> Any:
101
+ raise subprocess.TimeoutExpired(cmd="docker", timeout=runtime_module.PROBE_TIMEOUT_S)
102
+
103
+ monkeypatch.setattr(shutil, "which", lambda _n: "/usr/bin/docker")
104
+ monkeypatch.setattr(subprocess, "run", _timeout)
105
+ probed = runtime_module._probe("docker")
106
+ assert probed.availability is Availability.NOT_RUNNING
107
+ assert "timed out" in probed.detail
108
+
109
+
110
+ def test_a_missing_binary_is_reported_as_absent(monkeypatch: pytest.MonkeyPatch) -> None:
111
+ monkeypatch.setattr(shutil, "which", lambda _n: None)
112
+ assert runtime_module._probe("docker").availability is Availability.NOT_INSTALLED
113
+
114
+
115
+ def test_detection_reports_every_candidate(monkeypatch: pytest.MonkeyPatch) -> None:
116
+ """Not just the first that works: `--check` cannot explain "nothing available" without
117
+ saying what was looked for."""
118
+ _fake_probe(monkeypatch, {})
119
+ assert [r.name for r in detect_runtimes()] == list(runtime_module.CANDIDATES)
120
+
121
+
122
+ # -- the task catalogue -------------------------------------------------------------------------
123
+
124
+
125
+ @pytest.mark.parametrize(
126
+ ("task_id", "executes"), [("humaneval", True), ("mbpp", True), ("mmlu", False)]
127
+ )
128
+ def test_executes_code_is_declared_per_task(task_id: str, executes: bool) -> None:
129
+ assert resolve_task(task_id).executes_code is executes
130
+
131
+
132
+ def test_an_unknown_task_is_refused_rather_than_assumed_safe() -> None:
133
+ """Accepting it would mean guessing `executes_code`, and the safe-looking guess is the
134
+ dangerous one."""
135
+ with pytest.raises(UserError, match="unknown task"):
136
+ resolve_task("some-new-agentic-benchmark")
137
+
138
+
139
+ def test_task_lookup_is_case_insensitive() -> None:
140
+ assert resolve_task("HumanEval").id == "humaneval"