toolbench 0.2.1__tar.gz → 0.4.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.
- toolbench-0.4.0/CHANGELOG.md +93 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/PKG-INFO +1 -1
- {toolbench-0.2.1 → toolbench-0.4.0}/pyproject.toml +1 -1
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_codex_runtime.py +7 -1
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_crash_classifier.py +40 -0
- toolbench-0.4.0/tests/test_export.py +100 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_metrics.py +12 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_paired_deltas.py +40 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_resume.py +17 -0
- toolbench-0.4.0/tests/test_runner_toolbase_protection.py +29 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_trial_loop.py +40 -7
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/__init__.py +1 -1
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/cli.py +195 -19
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/crash_classifier.py +41 -1
- toolbench-0.4.0/toolbench/core/export.py +288 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/failure_modes.py +31 -1
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/metrics.py +9 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/runner.py +42 -2
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/runtime.py +134 -2
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/summary_text.py +54 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/reference/commands.md +31 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/.gitignore +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/LICENSE +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/calculator/requirements.txt +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/calculator/toolkit.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/calculator/tools/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/calculator/tools/basic.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/calculator/tools/scientific.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/benchmark.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/ground_truth/answer.json +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/harnesses/orchestral/anthropic.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/harnesses/orchestral/groq.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/harnesses/orchestral/litellm.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/harnesses/orchestral/openai.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/additive_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/all_metrics.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/calc_toolbase.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/core_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/full_local.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/full_mixed.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/full_toolbase.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/guided.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/loadouts/primitives_only.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/skills/distance_recipe.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/tools/chebyshev.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/tools/dunderkit.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/tools/euclid.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/tools/extras.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/tools/taxicab.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/derived/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/derived/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/derived/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/direct/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/direct/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/direct/sandbox/template/points.json +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/direct/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/polar/prompts/system.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/polar/prompts/user.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/examples/geometry/variants/polar/variant.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/claude-code/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/claude-code/default.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/codex/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/codex/default.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/README.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/anthropic.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/google.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/groq.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/litellm.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/harness_templates/orchestral/openai.yaml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/mkdocs.yml +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/helpers.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_artifact_cleanup.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_artifact_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_benchmark_extends.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_cli_runtime_stderr_drain.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_content_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_cost_extraction.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_geometry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_integrity.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_json_repair.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_judge_score.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_judge_select.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_mcp_source.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_numeric_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_rubric_loader.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runner_llm_judge.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runner_rate_limit.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runner_request_params.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runner_retry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runner_transient.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runtime_registry.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_runtime_version_label.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_schema.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_skills.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_summary_text_cost.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_tool_error_visibility.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_tool_resolver.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_toolbase_sandbox_scoping.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_toolbase_select_provenance.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/tests/test_variant.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/artifact_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/benchmark.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/budget.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/content_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/harness.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/integrity.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/json_repair.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/judge.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/judge_select.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/litellm_pricing.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/llm_factory.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/loadout.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/numeric_checks.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/skills.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/store.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/task.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/tool_policy.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/tool_resolver.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/trajectory.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/core/variant.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/__init__.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/_output.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/_shared.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/audit_log.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/k_sweep.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/parallel_coords.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/per_stage_k.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/plot_overview.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/toolbench/reporting/transcript.py +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/authoring/custom-tools.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/authoring/define-a-benchmark.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/authoring/overview.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/authoring/rubrics-and-checks.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/explanation.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/harnesses.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/install-and-quickstart.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/loadouts.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/reading-results.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/running-a-benchmark.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/toolbase.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/guides/variants.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/index.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/reference/files-and-layout.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/reference/metrics.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/reference/schemas.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/reference/vocabulary.md +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/stylesheets/extra.css +0 -0
- {toolbench-0.2.1 → toolbench-0.4.0}/userdocs/troubleshooting.md +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `toolbench` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.4.0] — 2026-07-28
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`toolbench export`** — turn a completed run into something you can share.
|
|
16
|
+
A run directory is a working artifact: gigabytes of transcripts and
|
|
17
|
+
intermediate data carrying absolute paths from the machine that produced it,
|
|
18
|
+
which is not a thing you can attach to a paper or render a results page from.
|
|
19
|
+
`export` writes two layers instead:
|
|
20
|
+
|
|
21
|
+
- `trials.jsonl` — one flat, denormalized, **schema-versioned** row per trial
|
|
22
|
+
(cell coordinates, score, pass/fail against the rubric's own threshold,
|
|
23
|
+
per-stage credits/weights/metrics, telemetry, provenance). Kilobytes.
|
|
24
|
+
Denormalized on purpose so a consumer needs no join logic and no knowledge
|
|
25
|
+
of toolbench's internal layout.
|
|
26
|
+
- `bundle/` — the graded evidence behind those rows: per-trial answer files,
|
|
27
|
+
audit logs, run summaries, manifest. Megabytes.
|
|
28
|
+
|
|
29
|
+
Transcripts are excluded by default (`--include-transcripts` opts in): they
|
|
30
|
+
dominate the size and, being binary, are copied verbatim rather than scrubbed.
|
|
31
|
+
Machine-specific absolute paths are rewritten to `${HOME}` / `${RUN}`
|
|
32
|
+
placeholders unless `--no-scrub` is passed. `--archive` also writes a
|
|
33
|
+
`.tar.gz`. On a representative 5-trial run this is 2.7 GB -> 13 MB (1.8 MB
|
|
34
|
+
compressed).
|
|
35
|
+
|
|
36
|
+
`schema_version` is the compatibility contract: additive changes bump the
|
|
37
|
+
minor, anything that moves or retypes an existing field bumps the major.
|
|
38
|
+
|
|
39
|
+
## [0.3.0] — 2026-07-24
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **MCP preflight verification.** Before any trial runs (and in `--dry-run`), a
|
|
44
|
+
run now verifies that every MCP-serving harness × tools-loadout can actually
|
|
45
|
+
reach its tools: it starts the loadout's `toolbase serve --profile …` exactly
|
|
46
|
+
as the CLI runtime would and completes an MCP `initialize` + `tools/list`
|
|
47
|
+
handshake. If the profile serves zero tools, the handshake times out, or the
|
|
48
|
+
resolved tools are missing, the run **aborts with exit 2 before executing a
|
|
49
|
+
single trial** and prints which cell failed. Previously a `tools` loadout that
|
|
50
|
+
could not reach its tools ran the entire arm silently tool-less and still
|
|
51
|
+
graded as a valid tools result. Verification is gated to the runtimes that
|
|
52
|
+
serve toolbase over MCP (`claude_code`, `codex`) via
|
|
53
|
+
`runtime_serves_toolbase_mcp`; in-process runtimes (orchestral) resolve tools
|
|
54
|
+
directly and are unaffected. New helpers: `verify_toolbase_mcp`,
|
|
55
|
+
`runtime_serves_toolbase_mcp`.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **`_toolbase_command()` now fails loudly.** When the `toolbase` executable
|
|
60
|
+
cannot be located (neither on `PATH` nor beside the running Python), it raises
|
|
61
|
+
with an actionable message instead of returning a bare `"toolbase"`. The bare
|
|
62
|
+
fallback silently produced a broken `.mcp.json` whose stdio server failed to
|
|
63
|
+
launch (`command not found` on the child's `PATH`), leaving the agent with
|
|
64
|
+
"No such tool available" while the trial still graded as if tools were
|
|
65
|
+
present — a failure mode that surfaced when an env's `toolbase` install was
|
|
66
|
+
in flux. Combined with the preflight above, an unresolvable toolbase is now
|
|
67
|
+
caught before the benchmark begins.
|
|
68
|
+
|
|
69
|
+
## [0.2.1] — 2026-07-24
|
|
70
|
+
|
|
71
|
+
Runtime and metrics hardening surfaced by the first publication benchmarking campaign.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **`SESSION_LIMIT` failure mode.** A subscription coding-agent CLI (`claude-code` / `codex`) that refuses a request because the logged-in account hit its plan's session/usage quota is no longer misfiled as an `AGENT_CRASH` scored 0. It is classified as a distinct `SESSION_LIMIT` mode (detected from the CLI limit text, checked before `RATE_LIMITED` so the API's `insufficient_quota` still maps to `RATE_LIMITED`), dropped from the scored population (reach / pass@k / stage funnel / paired deltas) via `EXCLUDED_FROM_METRICS`, and re-run on `resume`. The runner aborts the remaining queue on the first one — every later trial would fail identically until the quota resets — and reports the abort reason. Excluded rows stay on disk and surface as per-cell and run-level excluded counts. ([#23](https://github.com/tonymenzo/toolbench/pull/23))
|
|
76
|
+
- `gpt-5.6-sol` subscription API-equivalent pricing entry for cost estimation.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **Benchmark agents can no longer read Toolbase toolkit sources off disk.** A trial's `protected_paths` now also deny the agent read access to editable toolkit checkouts (`source_path` in each cached toolkit's `.install_meta.yaml`) and toolkit runtime data directories (`base_directory` in toolkit configs). Without this, an agent in a `core_only` control arm could bypass the MCP interface and import the real tool implementations straight off the filesystem, contaminating the control. MCP server children are not confined by the agent's shell filesystem profile, so declared Toolbase loadouts still execute normally.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **`claude-code` runtime ignored the `--models` sweep** — every cell ran the harness provider's default model instead of the requested one, so a model axis silently collapsed to a single model. The runtime now honors the swept model.
|
|
85
|
+
- **Toolbase MCP command resolution.** Child MCP processes launched a bare `toolbase` command, which failed when Toolbench was started with an absolute virtualenv Python whose `bin/` was not on `PATH`. The command is now resolved to an absolute path (`PATH` → the executable beside the running Python → bare command as a last-resort fallback so the CLI can report a normal MCP startup error).
|
|
86
|
+
|
|
87
|
+
## [0.2.0] — 2026-06-05
|
|
88
|
+
|
|
89
|
+
- Continuous / non-gating scoring, `codex` and `claude-code` subscription runtimes, selectable LLM judge, trajectory integrity auditor, subscription cost estimation, and parallel-run hardening.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
Versions before 0.2.0 predate this changelog; see the git history and release commit subjects for their contents.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toolbench
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A platform and CLI for building benchmarks for agentic tools and harnesses.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tonymenzo/toolbench
|
|
6
6
|
Project-URL: Repository, https://github.com/tonymenzo/toolbench
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from toolbench.core.harness import Harness
|
|
6
6
|
from toolbench.core.llm_factory import SubscriptionLLM
|
|
7
|
-
from toolbench.core.runtime import CodexAgent, _codex_factory
|
|
7
|
+
from toolbench.core.runtime import CodexAgent, _codex_factory, _toolbase_command
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _agent(tmp_path: Path, **kw) -> CodexAgent:
|
|
@@ -29,6 +29,12 @@ def test_toolbase_mcp_is_auto_approved_for_noninteractive_exec(tmp_path):
|
|
|
29
29
|
assert any("default_tools_approval_mode=\"approve\"" in x for x in args)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def test_toolbase_mcp_command_resolves_to_an_executable():
|
|
33
|
+
command = Path(_toolbase_command())
|
|
34
|
+
assert command.is_absolute()
|
|
35
|
+
assert command.is_file()
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
def test_resume_uses_only_resume_supported_flags(tmp_path):
|
|
33
39
|
a = _agent(tmp_path)
|
|
34
40
|
a.thread_id = "thread-123"
|
|
@@ -9,6 +9,7 @@ sys.path.insert(0, str(REPO_ROOT))
|
|
|
9
9
|
from toolbench.core.crash_classifier import classify_crash # noqa: E402
|
|
10
10
|
from toolbench.core.failure_modes import ( # noqa: E402
|
|
11
11
|
AGENT_CRASH, CONTEXT_LENGTH_EXCEEDED, MODEL_FORMAT_CRASH, RATE_LIMITED,
|
|
12
|
+
SESSION_LIMIT,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -155,6 +156,45 @@ class TestRateLimitCrash(unittest.TestCase):
|
|
|
155
156
|
self.assertEqual(mode, MODEL_FORMAT_CRASH)
|
|
156
157
|
|
|
157
158
|
|
|
159
|
+
class TestSessionLimitCrash(unittest.TestCase):
|
|
160
|
+
"""A subscription coding-agent CLI hitting the account's plan quota is a
|
|
161
|
+
SESSION_LIMIT — distinct from a 429 throttle and never a capability
|
|
162
|
+
failure. Checked before RATE_LIMITED so it can't be misfiled as one."""
|
|
163
|
+
|
|
164
|
+
def test_claude_code_session_limit_message(self):
|
|
165
|
+
# Verbatim shape of the claude-code CLI's is_error limit response.
|
|
166
|
+
exc = RuntimeError(
|
|
167
|
+
"claude_code runtime: claude reported is_error (success): "
|
|
168
|
+
"You've hit your session limit · resets 4:20am (America/Chicago)")
|
|
169
|
+
mode, reason = classify_crash(exc, "")
|
|
170
|
+
self.assertEqual(mode, SESSION_LIMIT)
|
|
171
|
+
self.assertIn("limit", reason.lower())
|
|
172
|
+
|
|
173
|
+
def test_usage_limit_message(self):
|
|
174
|
+
exc = RuntimeError("You've reached your usage limit for this plan")
|
|
175
|
+
mode, _ = classify_crash(exc, "")
|
|
176
|
+
self.assertEqual(mode, SESSION_LIMIT)
|
|
177
|
+
|
|
178
|
+
def test_session_limit_wins_over_rate_limit(self):
|
|
179
|
+
# A message mentioning both must classify as SESSION_LIMIT (checked
|
|
180
|
+
# first), not RATE_LIMITED — a quota reset is not a transient throttle.
|
|
181
|
+
exc = RuntimeError("session limit reached (rate limit 429)")
|
|
182
|
+
mode, _ = classify_crash(exc, "")
|
|
183
|
+
self.assertEqual(mode, SESSION_LIMIT)
|
|
184
|
+
|
|
185
|
+
def test_api_insufficient_quota_stays_rate_limited(self):
|
|
186
|
+
# The API's billing-quota error must NOT be swept into SESSION_LIMIT;
|
|
187
|
+
# it remains a RATE_LIMITED (retried/backed-off) outcome.
|
|
188
|
+
exc = RuntimeError("You exceeded your current quota: insufficient_quota")
|
|
189
|
+
mode, _ = classify_crash(exc, "")
|
|
190
|
+
self.assertEqual(mode, RATE_LIMITED)
|
|
191
|
+
|
|
192
|
+
def test_unrelated_crash_is_not_session_limit(self):
|
|
193
|
+
exc = RuntimeError("ValueError: could not parse geometry.yaml")
|
|
194
|
+
mode, _ = classify_crash(exc, "")
|
|
195
|
+
self.assertEqual(mode, AGENT_CRASH)
|
|
196
|
+
|
|
197
|
+
|
|
158
198
|
class TestProviderRejectedToolCall(unittest.TestCase):
|
|
159
199
|
"""Groq validates tool-call arguments server-side.
|
|
160
200
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Export contract tests.
|
|
2
|
+
|
|
3
|
+
The export is what leaves the machine, so the two properties worth pinning are
|
|
4
|
+
(1) the flat schema stays stable and self-describing, and (2) scrubbing actually
|
|
5
|
+
removes machine-specific paths — a silent regression there publishes someone's
|
|
6
|
+
home directory.
|
|
7
|
+
"""
|
|
8
|
+
import gzip
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from toolbench.core.export import SCHEMA_VERSION, export_run
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_run(tmp_path: Path, home_marker: str) -> Path:
|
|
18
|
+
run = tmp_path / "runs" / "2026-01-01T00-00-00_demo_stub_run"
|
|
19
|
+
(run / "trials" / "core_only__n000__seed1001" / "artifacts" / "results").mkdir(
|
|
20
|
+
parents=True)
|
|
21
|
+
trial_dir = run / "trials" / "core_only__n000__seed1001"
|
|
22
|
+
|
|
23
|
+
(run / "trials.jsonl").write_text(json.dumps({
|
|
24
|
+
"trial_id": "core_only__n000__seed1001",
|
|
25
|
+
"model": "stub", "harness": "h/default", "loadout": "core_only",
|
|
26
|
+
"variant": "v", "seed": 1001, "ok": True, "score": 0.95,
|
|
27
|
+
"stages": {"a": True}, "stage_credits": {"a": 1.0},
|
|
28
|
+
"wall_clock_s": 12.5, "cost_usd": 0.25, "output_tokens": 100,
|
|
29
|
+
"tool_calls": 3, "tool_errors": 0, "failure_mode": None,
|
|
30
|
+
}) + "\n")
|
|
31
|
+
(run / "manifest.json").write_text(json.dumps({
|
|
32
|
+
"run_id": run.name, "task": "demo", "versions": {"toolbench": "0.3.0"},
|
|
33
|
+
}))
|
|
34
|
+
(run / "summary.json").write_text(json.dumps({
|
|
35
|
+
"reach_weights": {"pass_threshold": 0.9},
|
|
36
|
+
}))
|
|
37
|
+
(run / "summary.txt").write_text(f"summary referencing {home_marker}/secret/path\n")
|
|
38
|
+
(trial_dir / "trial.json").write_text(json.dumps({
|
|
39
|
+
"grade": {"stage_grades": [
|
|
40
|
+
{"id": "a", "weight": 1.0, "credit": 1.0, "evidence": "ok",
|
|
41
|
+
"metrics": {"check_a": {"closeness": 0.5, "distance": 1.25,
|
|
42
|
+
"per_item": {"x": 1}}}},
|
|
43
|
+
]}
|
|
44
|
+
}))
|
|
45
|
+
(trial_dir / "audit.txt").write_text(f"ran in {home_marker}/work/sandbox\n")
|
|
46
|
+
(trial_dir / "artifacts" / "results" / "answer.json").write_text('{"v": 1}')
|
|
47
|
+
with gzip.open(trial_dir / "transcript.jsonl.gz", "wt") as fh:
|
|
48
|
+
fh.write('{"big": "transcript"}\n')
|
|
49
|
+
return run
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_flat_schema_and_scrub(tmp_path):
|
|
53
|
+
home = str(Path.home())
|
|
54
|
+
run = _make_run(tmp_path, home)
|
|
55
|
+
out = tmp_path / "export"
|
|
56
|
+
rep = export_run(run, out, scrub=True)
|
|
57
|
+
|
|
58
|
+
assert rep["n_trials"] == 1
|
|
59
|
+
rows = [json.loads(l) for l in (out / "trials.jsonl").read_text().splitlines()]
|
|
60
|
+
r = rows[0]
|
|
61
|
+
|
|
62
|
+
# (1) schema is self-describing and carries the cell coordinates flat
|
|
63
|
+
assert r["schema_version"] == SCHEMA_VERSION
|
|
64
|
+
for key in ("run_id", "trial_id", "harness", "loadout", "variant", "model",
|
|
65
|
+
"seed", "score", "pass_threshold", "passed", "stage_credits",
|
|
66
|
+
"stage_weights", "stage_metrics", "tokens", "provenance"):
|
|
67
|
+
assert key in r, f"missing {key}"
|
|
68
|
+
|
|
69
|
+
# pass/fail is resolved from the rubric threshold, not hardcoded
|
|
70
|
+
assert r["pass_threshold"] == 0.9
|
|
71
|
+
assert r["passed"] is True
|
|
72
|
+
|
|
73
|
+
# metrics are flattened out of their per-check nesting; bulk dicts dropped
|
|
74
|
+
assert r["stage_metrics"]["a"]["closeness"] == 0.5
|
|
75
|
+
assert "per_item" not in r["stage_metrics"]["a"]
|
|
76
|
+
|
|
77
|
+
# (2) no home path survives anywhere in the text output
|
|
78
|
+
for p in list(out.rglob("*")):
|
|
79
|
+
if p.is_file() and p.suffix != ".gz":
|
|
80
|
+
assert home not in p.read_text(), f"unscrubbed home path in {p}"
|
|
81
|
+
|
|
82
|
+
# transcripts excluded by default
|
|
83
|
+
assert not list((out / "bundle").rglob("transcript.jsonl.gz"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_transcripts_opt_in_and_no_scrub_flag(tmp_path):
|
|
87
|
+
home = str(Path.home())
|
|
88
|
+
run = _make_run(tmp_path, home)
|
|
89
|
+
out = tmp_path / "export2"
|
|
90
|
+
export_run(run, out, scrub=False, include_transcripts=True)
|
|
91
|
+
assert list((out / "bundle").rglob("transcript.jsonl.gz"))
|
|
92
|
+
# with scrubbing off the raw path is preserved (so the flag is meaningful)
|
|
93
|
+
assert home in (out / "bundle" / "summary.txt").read_text()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_rejects_incomplete_run(tmp_path):
|
|
97
|
+
empty = tmp_path / "runs" / "not_a_run"
|
|
98
|
+
empty.mkdir(parents=True)
|
|
99
|
+
with pytest.raises(FileNotFoundError):
|
|
100
|
+
export_run(empty, tmp_path / "out")
|
|
@@ -242,6 +242,18 @@ class TestCostUsd(unittest.TestCase):
|
|
|
242
242
|
self.assertAlmostEqual(estimate["usd"], 56.0)
|
|
243
243
|
self.assertTrue(estimate["long_context_pricing_applied"])
|
|
244
244
|
|
|
245
|
+
def test_gpt56_sol_subscription_api_equivalent(self):
|
|
246
|
+
estimate = subscription_api_equivalent_cost(
|
|
247
|
+
"gpt-5.6-sol",
|
|
248
|
+
input_tokens=1_000_000,
|
|
249
|
+
cache_read_tokens=1_000_000,
|
|
250
|
+
output_tokens=1_000_000,
|
|
251
|
+
initial_input_tokens=272_001,
|
|
252
|
+
)
|
|
253
|
+
self.assertAlmostEqual(estimate["usd"], 56.0)
|
|
254
|
+
self.assertTrue(estimate["long_context_pricing_applied"])
|
|
255
|
+
self.assertIn("gpt-5.6-sol", estimate["source"])
|
|
256
|
+
|
|
245
257
|
def test_unknown_subscription_model_is_not_guessed(self):
|
|
246
258
|
self.assertIsNone(subscription_api_equivalent_cost("future-model"))
|
|
247
259
|
|
|
@@ -78,5 +78,45 @@ class TestPairedDeltas(unittest.TestCase):
|
|
|
78
78
|
self.assertAlmostEqual(summary["paired_deltas"][0]["reach_delta"], 0.45)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
class TestExcludedFromMetrics(unittest.TestCase):
|
|
82
|
+
"""SESSION_LIMIT trials are dropped from the scored population (n / reach /
|
|
83
|
+
pass / paired deltas) but surfaced as an explicit excluded count, so a
|
|
84
|
+
subscription-quota termination never pollutes the metrics as a score-0."""
|
|
85
|
+
|
|
86
|
+
def _rows(self):
|
|
87
|
+
from toolbench.core.failure_modes import SESSION_LIMIT
|
|
88
|
+
rows = []
|
|
89
|
+
# cell (m, A): 2 genuine passes (reach 1.0) + 2 quota terminations.
|
|
90
|
+
for seed in (1, 2):
|
|
91
|
+
rows.append(_row("m", "A", seed, 1, 1))
|
|
92
|
+
for seed in (3, 4):
|
|
93
|
+
rows.append(_row("m", "A", seed, 0, 0,
|
|
94
|
+
failure_mode=SESSION_LIMIT, stages={}, score=0.0))
|
|
95
|
+
# cell (m, B): 2 genuine trials sharing seeds 1,2 with A.
|
|
96
|
+
for seed in (1, 2):
|
|
97
|
+
rows.append(_row("m", "B", seed, 1, 0))
|
|
98
|
+
return rows
|
|
99
|
+
|
|
100
|
+
def test_excluded_from_scored_population(self):
|
|
101
|
+
summary = aggregate(self._rows(), k=4, stage_order=STAGE_ORDER,
|
|
102
|
+
stage_weights=WEIGHTS)
|
|
103
|
+
self.assertEqual(summary["n_total_trials"], 4) # 2 A + 2 B scored
|
|
104
|
+
self.assertEqual(summary["n_excluded_trials"], 2) # 2 quota rows
|
|
105
|
+
cell_a = next(c for c in summary["cells"] if c["condition"] == "A")
|
|
106
|
+
self.assertEqual(cell_a["n"], 2)
|
|
107
|
+
self.assertEqual(cell_a["n_excluded"], 2)
|
|
108
|
+
# Reach reflects ONLY the 2 genuine passes, not diluted to 0.5 by the
|
|
109
|
+
# 2 excluded score-0 quota rows.
|
|
110
|
+
self.assertAlmostEqual(cell_a["reach_bar_k"], 1.0)
|
|
111
|
+
|
|
112
|
+
def test_excluded_not_in_paired_deltas(self):
|
|
113
|
+
# The A−B delta pairs only scored seeds (1, 2); A's excluded quota
|
|
114
|
+
# rows on seeds 3, 4 must not create phantom pairs or shift the delta.
|
|
115
|
+
summary = aggregate(self._rows(), k=4, stage_order=STAGE_ORDER,
|
|
116
|
+
stage_weights=WEIGHTS)
|
|
117
|
+
self.assertEqual(len(summary["paired_deltas"]), 1)
|
|
118
|
+
self.assertEqual(summary["paired_deltas"][0]["n_pairs"], 2)
|
|
119
|
+
|
|
120
|
+
|
|
81
121
|
if __name__ == "__main__":
|
|
82
122
|
unittest.main()
|
|
@@ -28,6 +28,23 @@ def test_partition_splits_resolution_errors():
|
|
|
28
28
|
assert [r["trial_id"] for r in retryable] == ["b"]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def test_partition_retries_session_limit():
|
|
32
|
+
# A subscription session/usage-quota termination took no real
|
|
33
|
+
# measurement, so a resume must re-run it (like resolution_error) rather
|
|
34
|
+
# than freezing its score-0 record. A real AGENT_CRASH stays kept.
|
|
35
|
+
from toolbench.core.failure_modes import SESSION_LIMIT
|
|
36
|
+
rows = [
|
|
37
|
+
{"trial_id": "a", "failure_mode": "NONE"},
|
|
38
|
+
{"trial_id": "b", "failure_mode": SESSION_LIMIT},
|
|
39
|
+
{"trial_id": "c", "failure_mode": "resolution_error"},
|
|
40
|
+
{"trial_id": "d", "failure_mode": "AGENT_CRASH"},
|
|
41
|
+
{"trial_id": "e", "failure_mode": "MODEL_STOPPED_EARLY"},
|
|
42
|
+
]
|
|
43
|
+
kept, retryable = _partition_resume_rows(rows)
|
|
44
|
+
assert [r["trial_id"] for r in kept] == ["a", "d", "e"]
|
|
45
|
+
assert sorted(r["trial_id"] for r in retryable) == ["b", "c"]
|
|
46
|
+
|
|
47
|
+
|
|
31
48
|
def test_budget_precharge_counts_toward_cap():
|
|
32
49
|
b = Budget(10.0)
|
|
33
50
|
b.precharge(9.5)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from toolbench.core.runner import _toolbase_protected_paths
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_toolbase_and_editable_sources_are_protected(tmp_path, monkeypatch):
|
|
7
|
+
toolbase_home = tmp_path / ".toolbase"
|
|
8
|
+
slot = toolbase_home / "cache" / "physics-kit" / "editable"
|
|
9
|
+
slot.mkdir(parents=True)
|
|
10
|
+
source = tmp_path / "physics-kit-source"
|
|
11
|
+
source.mkdir()
|
|
12
|
+
runtime_data = tmp_path / "physics-kit-runtime"
|
|
13
|
+
runtime_data.mkdir()
|
|
14
|
+
(slot / ".install_meta.yaml").write_text(
|
|
15
|
+
"editable: true\n"
|
|
16
|
+
f"source_path: {source}\n"
|
|
17
|
+
)
|
|
18
|
+
monkeypatch.setenv("TOOLBASE_HOME", str(toolbase_home))
|
|
19
|
+
config_dir = toolbase_home / "config"
|
|
20
|
+
config_dir.mkdir()
|
|
21
|
+
(config_dir / "physics-kit.yaml").write_text(
|
|
22
|
+
f"base_directory: {runtime_data}\n"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
paths = _toolbase_protected_paths()
|
|
26
|
+
|
|
27
|
+
assert str(toolbase_home.resolve()) in paths
|
|
28
|
+
assert str(source.resolve()) in paths
|
|
29
|
+
assert str(runtime_data.resolve()) in paths
|
|
@@ -49,10 +49,13 @@ class _FakeRunner:
|
|
|
49
49
|
"""Stands in for TrialRunner: records execution, optionally charges
|
|
50
50
|
the budget per trial so abort behavior can be exercised."""
|
|
51
51
|
|
|
52
|
-
def __init__(self, cost_per_trial=0.0, barrier=None):
|
|
52
|
+
def __init__(self, cost_per_trial=0.0, barrier=None, fail_mode=None):
|
|
53
53
|
self.cost = cost_per_trial
|
|
54
54
|
self.ran = []
|
|
55
55
|
self.barrier = barrier
|
|
56
|
+
# When set, every trial returns this failure_mode with score 0 (used to
|
|
57
|
+
# exercise the session-limit abort path).
|
|
58
|
+
self.fail_mode = fail_mode
|
|
56
59
|
self._lock = threading.Lock()
|
|
57
60
|
|
|
58
61
|
def run_trial(self, *, model_cfg, benchmark, harness, loadout, variant,
|
|
@@ -75,6 +78,14 @@ class _FakeRunner:
|
|
|
75
78
|
aborted = True
|
|
76
79
|
from toolbench.core.task import Grade
|
|
77
80
|
from toolbench.core.trajectory import Trajectory
|
|
81
|
+
if self.fail_mode is not None:
|
|
82
|
+
grade = Grade(score=0.0, stages={}, stage_grades=[],
|
|
83
|
+
failure_mode=self.fail_mode, judge_kind="rule")
|
|
84
|
+
return SimpleNamespace(
|
|
85
|
+
trial_id=trial_id, ok=False, score=0.0, grade=grade,
|
|
86
|
+
trajectory=Trajectory(), wall_clock_s=0.0, cost_usd=self.cost,
|
|
87
|
+
aborted_by_budget=aborted, error=None, attempts=1, nudges=0,
|
|
88
|
+
rate_limit_retries=0, transient_retries=0)
|
|
78
89
|
grade = Grade(score=1.0, stages={"s0": True}, stage_grades=[],
|
|
79
90
|
failure_mode="NONE", judge_kind="rule")
|
|
80
91
|
return SimpleNamespace(
|
|
@@ -98,17 +109,19 @@ class TestRunTrialLoop(unittest.TestCase):
|
|
|
98
109
|
|
|
99
110
|
def test_serial_runs_everything(self):
|
|
100
111
|
runner = _FakeRunner()
|
|
101
|
-
records, aborted = self._loop(runner, Budget(None))
|
|
112
|
+
records, aborted, reason = self._loop(runner, Budget(None))
|
|
102
113
|
self.assertEqual(len(records), 6)
|
|
103
114
|
self.assertFalse(aborted)
|
|
115
|
+
self.assertIsNone(reason)
|
|
104
116
|
|
|
105
117
|
def test_budget_abort_is_balanced_across_cells(self):
|
|
106
118
|
# $1/trial, $3.5 cap → abort fires on trial 4 (seed-major order:
|
|
107
119
|
# A1 B1 A2 B2 ...), leaving each loadout with the SAME number of
|
|
108
120
|
# completed trials instead of one loadout hogging the budget.
|
|
109
121
|
runner = _FakeRunner(cost_per_trial=1.0)
|
|
110
|
-
records, aborted = self._loop(runner, Budget(3.5))
|
|
122
|
+
records, aborted, reason = self._loop(runner, Budget(3.5))
|
|
111
123
|
self.assertTrue(aborted)
|
|
124
|
+
self.assertEqual(reason, "budget")
|
|
112
125
|
per_cell = {}
|
|
113
126
|
for r in records:
|
|
114
127
|
if not r["aborted_by_budget"]:
|
|
@@ -119,22 +132,42 @@ class TestRunTrialLoop(unittest.TestCase):
|
|
|
119
132
|
def test_parallel_runs_everything_and_is_concurrent(self):
|
|
120
133
|
barrier = threading.Barrier(2)
|
|
121
134
|
runner = _FakeRunner(barrier=barrier)
|
|
122
|
-
records, aborted = self._loop(runner, Budget(None), parallel=2,
|
|
123
|
-
|
|
135
|
+
records, aborted, reason = self._loop(runner, Budget(None), parallel=2,
|
|
136
|
+
n_loadouts=2, seeds=(1, 2, 3))
|
|
124
137
|
# The barrier requires 2 trials in flight simultaneously; if the
|
|
125
138
|
# loop were serial this would deadlock (and time out the wait).
|
|
126
139
|
self.assertEqual(len(records), 6)
|
|
127
140
|
self.assertFalse(aborted)
|
|
141
|
+
self.assertIsNone(reason)
|
|
128
142
|
self.assertEqual(len(runner.ran), 6)
|
|
129
143
|
|
|
130
144
|
def test_parallel_budget_abort_stops_launching(self):
|
|
131
145
|
runner = _FakeRunner(cost_per_trial=1.0)
|
|
132
|
-
records, aborted = self._loop(runner, Budget(2.5), parallel=2,
|
|
133
|
-
|
|
146
|
+
records, aborted, reason = self._loop(runner, Budget(2.5), parallel=2,
|
|
147
|
+
seeds=(1, 2, 3, 4, 5))
|
|
134
148
|
self.assertTrue(aborted)
|
|
149
|
+
self.assertEqual(reason, "budget")
|
|
135
150
|
# 10 trials enumerated; the abort must prevent most of the tail.
|
|
136
151
|
self.assertLess(len(runner.ran), 10)
|
|
137
152
|
|
|
153
|
+
def test_session_limit_aborts_and_stops_launching(self):
|
|
154
|
+
# A subscription session/usage-quota termination must halt the queue
|
|
155
|
+
# (every remaining trial would fail identically until the quota
|
|
156
|
+
# resets) and report the reason as "session_limit", not "budget".
|
|
157
|
+
# cost_per_trial slows each trial (as in the budget tests) so the
|
|
158
|
+
# abort demonstrably beats the queue draining; Budget(None) keeps the
|
|
159
|
+
# abort attributable to the session limit, not the budget.
|
|
160
|
+
runner = _FakeRunner(cost_per_trial=1.0, fail_mode="SESSION_LIMIT")
|
|
161
|
+
records, aborted, reason = self._loop(runner, Budget(None), parallel=2,
|
|
162
|
+
seeds=(1, 2, 3, 4, 5))
|
|
163
|
+
self.assertTrue(aborted)
|
|
164
|
+
self.assertEqual(reason, "session_limit")
|
|
165
|
+
# 10 trials enumerated; the abort must prevent most of the tail from
|
|
166
|
+
# ever launching (only the already-in-flight one(s) get recorded).
|
|
167
|
+
self.assertLess(len(runner.ran), 10)
|
|
168
|
+
self.assertTrue(all(r["failure_mode"] == "SESSION_LIMIT"
|
|
169
|
+
for r in records))
|
|
170
|
+
|
|
138
171
|
|
|
139
172
|
if __name__ == "__main__":
|
|
140
173
|
unittest.main()
|