headroom/tests/test_binaries.py
Tejas Chopra 93f2d7a2da
chore: release main (#2792)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.35.0</summary>

##
[0.35.0](https://github.com/headroomlabs-ai/headroom/compare/v0.34.0...v0.35.0)
(2026-08-12)


### Features

* **beacon:** allowlist the routing summary key
([#2818](https://github.com/headroomlabs-ai/headroom/issues/2818))
([7940c05](7940c05ebf))
* **beacon:** hourly R2 compaction, per-strategy savings, and a stack
that reports
([#2853](https://github.com/headroomlabs-ai/headroom/issues/2853))
([e0870ef](e0870ef931))
* **cli,pricing:** add CLI extension seam and prompt-cache TTL pricing
([#2802](https://github.com/headroomlabs-ai/headroom/issues/2802))
([6ec3e34](6ec3e3478a))


### Bug Fixes

* **anthropic:** strip first-party tool search on custom upstreams
([#2539](https://github.com/headroomlabs-ai/headroom/issues/2539))
([7f6950b](7f6950be34))
* **backends/anyllm:** convert Anthropic tools and tool_choice to OpenAI
shape
([0d6866b](0d6866b91a))
* **backends/anyllm:** stream tool_use blocks and map finish_reason on
the streaming path
([e4904e2](e4904e23a6))
* **backends/litellm:** None-guard core token counts in OpenAI usage
block ([#2324](https://github.com/headroomlabs-ai/headroom/issues/2324))
([12f9f58](12f9f58cb3))
* **beacon:** report all-layers savings, not context-compression only
([#2796](https://github.com/headroomlabs-ai/headroom/issues/2796))
([e9a24f3](e9a24f3ec1))
* **beacon:** split session failures by status code
([#2815](https://github.com/headroomlabs-ai/headroom/issues/2815))
([2954e37](2954e37048))
* **cache:** bound compression cache bookkeeping
([0ae948c](0ae948c151))
* **cache:** enforce Anthropic's 1h-before-5m cache_control ordering
before forwarding
([#2941](https://github.com/headroomlabs-ai/headroom/issues/2941))
([3752458](3752458022))
* **cache:** mirror client cache_control positions instead of
single-marker consolidation
([def3d76](def3d76e5a))
* **cache:** stabilize Anthropic block-growing lineages
([#2917](https://github.com/headroomlabs-ai/headroom/issues/2917))
([1a04c95](1a04c957f5))
* **ccr:** avoid injecting tool on chat streaming
([d0c1f5b](d0c1f5b8ad))
* **ccr:** preserve exact SQLite TTL boundary
([#2669](https://github.com/headroomlabs-ai/headroom/issues/2669))
([d0a86d4](d0a86d409f))
* **ccr:** report embedded hashes from compress endpoint
([#717](https://github.com/headroomlabs-ai/headroom/issues/717))
([685ebe4](685ebe457d))
* **ccr:** resolve &lt;&lt;ccr:...&gt;&gt; markers inline when no
retrieve-tool path exists
([#2512](https://github.com/headroomlabs-ai/headroom/issues/2512))
([ce8ce83](ce8ce8313f))
* **ccr:** tolerate null/malformed OpenAI data in response handling
([#2467](https://github.com/headroomlabs-ai/headroom/issues/2467))
([e583e08](e583e082d8))
* **ci:** publish latest from the root Docker manifest
([#2252](https://github.com/headroomlabs-ai/headroom/issues/2252))
([5568d73](5568d738af))
* **claude:** stop forcing tool search on Foundry
([#2477](https://github.com/headroomlabs-ai/headroom/issues/2477))
([7981396](798139608c))
* **cli/update:** let install ownership win over bare /.dockerenv so
venv installs self-update
([#2830](https://github.com/headroomlabs-ai/headroom/issues/2830))
([7092b53](7092b53c46))
* **codex:** route alpha search through the Codex backend
([#2538](https://github.com/headroomlabs-ai/headroom/issues/2538))
([a540eb2](a540eb2c61))
* **content-router:** protect custom-tag blocks before mixed-content
section split
([d7bc1e2](d7bc1e275f))
* **deps:** bump h2 to 4.4.1 for CVE-2026-71554
([#2839](https://github.com/headroomlabs-ai/headroom/issues/2839))
([564e0a8](564e0a8d0f))
* **deps:** enforce audited transitive dependency floors
([#2791](https://github.com/headroomlabs-ai/headroom/issues/2791))
([64e2039](64e203931b))
* **doctor:** flag `ollama launch claude` proxy bypass instead of
misdirecting
([#2566](https://github.com/headroomlabs-ai/headroom/issues/2566))
([7f24d69](7f24d695ee))
* emit SSE ping before message_start on Bedrock streaming path (issue
[#902](https://github.com/headroomlabs-ai/headroom/issues/902))
([#1080](https://github.com/headroomlabs-ai/headroom/issues/1080))
([4dab254](4dab254d52))
* **gemini:** resolve native CCR retrieval calls
([#2253](https://github.com/headroomlabs-ai/headroom/issues/2253))
([2483f57](2483f57002))
* **health:** label kompress as degraded/optional when not yet loaded
([#2865](https://github.com/headroomlabs-ai/headroom/issues/2865))
([8949371](89493714d2))
* **image:** decouple routing types from trained_router so importing the
compressor doesn't import torch
([#2513](https://github.com/headroomlabs-ai/headroom/issues/2513))
([#2537](https://github.com/headroomlabs-ai/headroom/issues/2537))
([d7cf981](d7cf981093))
* **install/windows:** register persistent-task from S4U hidden XML
([#2453](https://github.com/headroomlabs-ai/headroom/issues/2453))
([#2459](https://github.com/headroomlabs-ai/headroom/issues/2459))
([1edaeb8](1edaeb8b76))
* **install:** don't crash the PowerShell installer when $PROFILE is
unset ([#2469](https://github.com/headroomlabs-ai/headroom/issues/2469))
([fc5c4e2](fc5c4e239c))
* **install:** trust Docker bridge for dashboard metadata
([e044139](e044139001))
* **install:** use --userns=keep-id under Podman so bind-mount writes
don't fail
([#2846](https://github.com/headroomlabs-ai/headroom/issues/2846))
([3488f8d](3488f8d4b5))
* **learn/gemini:** stop double-counting session tokens
([#2230](https://github.com/headroomlabs-ai/headroom/issues/2230))
([29d8a5e](29d8a5e563))
* **learn/grok:** detect a Windows absolute project path
([#2283](https://github.com/headroomlabs-ai/headroom/issues/2283))
([e240df2](e240df2b69))
* **learn:** stop classifying a successful exit code 0 as an error
([#2289](https://github.com/headroomlabs-ai/headroom/issues/2289))
([a24fe7d](a24fe7dcbf))
* **litellm:** add async_post_call_success_hook to HeadroomCallback
([#1322](https://github.com/headroomlabs-ai/headroom/issues/1322))
([3107994](3107994aed))
* **litellm:** don't forward a caller key the target cannot accept
([#2883](https://github.com/headroomlabs-ai/headroom/issues/2883))
([2f2950a](2f2950a626))
* **memory:** bound the TrafficLearner pending-pattern accumulator
(memory leak)
([#2579](https://github.com/headroomlabs-ai/headroom/issues/2579))
([1f5feff](1f5fefffd3))
* **memory:** close DirectMem0 resources
([6596182](65961827cf))
* **memory:** close MCP backend on shutdown
([4bd8ecd](4bd8ecd1e3))
* **memory:** don't crash inline memory extraction on a non-object
&lt;memory&gt; block
([#2470](https://github.com/headroomlabs-ai/headroom/issues/2470))
([e00c6ff](e00c6ff81c))
* **memory:** keep vector metadata in sync
([#2295](https://github.com/headroomlabs-ai/headroom/issues/2295))
([c471800](c471800e8e))
* **memory:** make explicit-project and user store keys
collision-resistant
([#2231](https://github.com/headroomlabs-ai/headroom/issues/2231))
([f840d5f](f840d5f2fe))
* **memory:** skip &lt;system-reminder&gt; blocks when building the
retrieval query
([#2195](https://github.com/headroomlabs-ai/headroom/issues/2195))
([#2541](https://github.com/headroomlabs-ai/headroom/issues/2541))
([4e5a67a](4e5a67a342))
* **memory:** sync FTS5 and vector indexes on CLI
delete/edit/prune/purge
([fd4628d](fd4628d821))
* **oauth2:** make repository lint checks pass
([c85abf7](c85abf7a87))
* **observability:** aggregate tool savings in OTEL
([#2936](https://github.com/headroomlabs-ai/headroom/issues/2936))
([941c25d](941c25d31e))
* **onnx:** stop ONNX thread pools from spinning idle cores
([#2495](https://github.com/headroomlabs-ai/headroom/issues/2495))
([#2540](https://github.com/headroomlabs-ai/headroom/issues/2540))
([5c561bd](5c561bd913))
* **openai:** skip Responses tool-search deferral for clients that
cannot execute it
([#2696](https://github.com/headroomlabs-ai/headroom/issues/2696))
([54ea28d](54ea28d983))
* **opencode:** ship the transport hook-shim so wheel installs route
Node child traffic
([702dbc5](702dbc5902))
* **providers/anthropic:** don't crash token estimation on null
tool_calls
([#2472](https://github.com/headroomlabs-ai/headroom/issues/2472))
([08466f3](08466f3cae))
* **providers/openai:** bound tiktoken vocab loads with the guarded
loader
([#2554](https://github.com/headroomlabs-ai/headroom/issues/2554))
([0805e8e](0805e8e410))
* **proxy/anthropic:** inject headroom_retrieve whenever a CCR marker is
present, not only for new markers
([#2848](https://github.com/headroomlabs-ai/headroom/issues/2848))
([3808f60](3808f60ca6))
* **proxy/anthropic:** None-guard usage token counts on the direct
buffered path
([#2434](https://github.com/headroomlabs-ai/headroom/issues/2434))
([2b5ee7c](2b5ee7cde8))
* **proxy/anthropic:** run tool-search history repair after turn hooks
([c6f9948](c6f99482e1))
* **proxy/batch:** don't crash an OpenAI batch on a valid-JSON
non-object line
([#2316](https://github.com/headroomlabs-ai/headroom/issues/2316))
([1f2c681](1f2c681c0b))
* **proxy/bedrock:** report uncached input tokens from backend usage,
not the live-zone count
([#2318](https://github.com/headroomlabs-ai/headroom/issues/2318))
([c19e412](c19e412b33))
* **proxy/gemini:** keep streaming-parity baseline so eligible_pct can't
exceed 100
([#2824](https://github.com/headroomlabs-ai/headroom/issues/2824))
([b97c7c6](b97c7c6e99))
* **proxy/metrics:** cap client-supplied model label cardinality
([#2480](https://github.com/headroomlabs-ai/headroom/issues/2480))
([e24a7e6](e24a7e66b9))
* **proxy/metrics:** escape label values in the Prometheus export
([#2463](https://github.com/headroomlabs-ai/headroom/issues/2463))
([6a53861](6a53861063))
* **proxy/openai:** don't crash the Responses memory tool loops on null
arguments
([#2273](https://github.com/headroomlabs-ai/headroom/issues/2273))
([a30db2c](a30db2cae4))
* **proxy/openai:** feed Codex WS traffic into the traffic learner
([#2334](https://github.com/headroomlabs-ai/headroom/issues/2334))
([f669149](f669149769))
* **proxy/openai:** run response hooks on Responses, and bill their
re-drives
([#2872](https://github.com/headroomlabs-ai/headroom/issues/2872))
([675d13f](675d13f08d))
* **proxy:** allow settings routes for trusted gateway/dashboard clients
([#2491](https://github.com/headroomlabs-ai/headroom/issues/2491))
([a5b0a8f](a5b0a8f4cc))
* **proxy:** cache litellm model resolution to stop repeated Provider
List spam
([99f07e7](99f07e7bbd))
* **proxy:** cancel periodic TOIN task on shutdown
([739fdef](739fdef423))
* **proxy:** close the upstream stream when a streaming body is never
consumed
([0951663](0951663562))
* **proxy:** compress cache-mode cold starts and tag prefix-mismatch
passthrough
([#2365](https://github.com/headroomlabs-ai/headroom/issues/2365))
([aaeba0a](aaeba0a319))
* **proxy:** emit request log timestamps in UTC
([620028f](620028fa18))
* **proxy:** enable tool search by default and repair poisoned
transcripts
([#2807](https://github.com/headroomlabs-ai/headroom/issues/2807))
([0237cbf](0237cbffbb))
* **proxy:** gate mid-turn message coalescing to Claude Code clients
([#1643](https://github.com/headroomlabs-ai/headroom/issues/1643))
([a4bd2e6](a4bd2e62a5))
* **proxy:** give each Codex /v1/responses WS turn a unique request_id
([#2164](https://github.com/headroomlabs-ai/headroom/issues/2164))
([d02df10](d02df10758))
* **proxy:** graceful shutdown and reliable Ctrl+C exit
([#621](https://github.com/headroomlabs-ai/headroom/issues/621))
([17cdb18](17cdb185bc))
* **proxy:** guard telemetry and TOIN endpoints
([cde1513](cde1513c91))
* **proxy:** include tool_search_deferral savings in the savings ledger
([12149f7](12149f7446))
* **proxy:** pass through cross-region prefixed Bedrock model IDs
directly
([#2330](https://github.com/headroomlabs-ai/headroom/issues/2330))
([64cb46e](64cb46e24b))
* **proxy:** port session-sticky beta headers to the Rust proxy
([#2381](https://github.com/headroomlabs-ai/headroom/issues/2381))
([f6398a6](f6398a6476))
* **proxy:** preserve merged session and quarantine contracts
([#2943](https://github.com/headroomlabs-ai/headroom/issues/2943))
([039cd24](039cd2431a))
* **proxy:** preserve signed Anthropic thinking blocks on outbound
re-serialize
([#2254](https://github.com/headroomlabs-ai/headroom/issues/2254))
([dc163bc](dc163bcd1c))
* **proxy:** stop discarding compressed Codex WS later-frame payloads
([#2823](https://github.com/headroomlabs-ai/headroom/issues/2823))
([4ec416d](4ec416df88))
* **proxy:** time-cap the compression timeout-debt quarantine
([#2360](https://github.com/headroomlabs-ai/headroom/issues/2360))
([#2412](https://github.com/headroomlabs-ai/headroom/issues/2412))
([c5a08d2](c5a08d22e0))
* **proxy:** unwrap Hermes tool_call bridge in tool name map
([#2717](https://github.com/headroomlabs-ai/headroom/issues/2717))
([a97b824](a97b82413b))
* publish headroom-opencode in release workflow
([#2372](https://github.com/headroomlabs-ai/headroom/issues/2372))
([7859154](78591545ce))
* **settings:** accept documented HEADROOM_* env names as settings keys
([#2833](https://github.com/headroomlabs-ai/headroom/issues/2833))
([de9e052](de9e0523da))
* **subscription:** dedup transcript usage by message id
([#2340](https://github.com/headroomlabs-ai/headroom/issues/2340) token
inflation)
([#2408](https://github.com/headroomlabs-ai/headroom/issues/2408))
([74275b7](74275b7c3e))
* **toin:** bound private query and pattern retention
([8cd1380](8cd138039e))
* **tokenizer:** coerce non-string tool_call fields before counting
([#2801](https://github.com/headroomlabs-ai/headroom/issues/2801))
([b6f9877](b6f9877c78))
* **tokenizer:** price CJK in the Rust fixed-ratio estimator (Python
parity)
([#2260](https://github.com/headroomlabs-ai/headroom/issues/2260))
([6840153](6840153473))
* **transforms/adaptive-sizer:** honor max_k on small-input fast path
([#2319](https://github.com/headroomlabs-ai/headroom/issues/2319))
([8a90523](8a90523209))
* **transforms/smart_crusher:** don't crash on a tool call with a null
function
([#2232](https://github.com/headroomlabs-ai/headroom/issues/2232))
([3bb02f8](3bb02f8f75))
* Vertex model pricing shows $0.00 for versioned model names and
vertex:anthropic provider
([#2517](https://github.com/headroomlabs-ai/headroom/issues/2517))
([eb5b5e4](eb5b5e4198))
* **wrap/claude:** keep --1m effective when an explicit --model is
passed through
([c093bf1](c093bf11eb))
* **wrap/opencode:** verify the opencode binary before mutating config
([ae38486](ae384862a4))
* **wrap/serena:** install Serena from the serena-agent PyPI wheel, not
the git source
([d7b25ae](d7b25ae3bb))
* **wrap:** honor Copilot OAuth wire-api override and model default
([#2387](https://github.com/headroomlabs-ai/headroom/issues/2387))
([1db6d88](1db6d88ab4))
* **wrap:** serialize shared proxy startup
([#2946](https://github.com/headroomlabs-ai/headroom/issues/2946))
([e540d64](e540d64feb))
* **wrap:** stop the launch cwd from shadowing the installed package in
the proxy subprocess
([#2843](https://github.com/headroomlabs-ai/headroom/issues/2843))
([c49be26](c49be269a1))


### Performance Improvements

* cut hot-path latency 27% (token-count memo, startup preloads, JSON
scan memo)
([#2838](https://github.com/headroomlabs-ai/headroom/issues/2838))
([53af90d](53af90d68c))
* **proxy:** bound upstream calls and hot-path costs
([#2852](https://github.com/headroomlabs-ai/headroom/issues/2852))
([f624d3a](f624d3a00a))
* **subscription:** skip transcripts older than the window in
compute_window_tokens
([#2861](https://github.com/headroomlabs-ai/headroom/issues/2861))
([91d6bf3](91d6bf33cd))


### Dependencies

* bump brace-expansion from 5.0.7 to 5.0.9 in /docs
([#2751](https://github.com/headroomlabs-ai/headroom/issues/2751))
([56ee57b](56ee57be98))
* bump bytesize from 1.3.3 to 2.4.2
([#2286](https://github.com/headroomlabs-ai/headroom/issues/2286))
([6448545](6448545a7f))
* bump hf-hub from 0.4.3 to 0.5.0
([#2285](https://github.com/headroomlabs-ai/headroom/issues/2285))
([4925bf6](4925bf6a82))
* bump next from 16.2.10 to 16.3.0 in /docs
([#2750](https://github.com/headroomlabs-ai/headroom/issues/2750))
([0fd0b99](0fd0b996a4))
* bump postcss from 8.5.19 to 8.5.25 in /plugins/openclaw
([#2749](https://github.com/headroomlabs-ai/headroom/issues/2749))
([cd60ee9](cd60ee9ae8))
* bump postcss from 8.5.19 to 8.5.25 in /plugins/opencode
([#2748](https://github.com/headroomlabs-ai/headroom/issues/2748))
([ff4e016](ff4e0167bb))
* bump postcss from 8.5.19 to 8.5.25 in /sdk/typescript
([#2747](https://github.com/headroomlabs-ai/headroom/issues/2747))
([267c2bd](267c2bdcb5))
* bump postcss from 8.5.19 to 8.5.26 in /docs
([#2881](https://github.com/headroomlabs-ai/headroom/issues/2881))
([e6e5826](e6e5826423))
* bump ruff from 0.15.17 to 0.15.22 in the pip-minor-patch group
([#2501](https://github.com/headroomlabs-ai/headroom/issues/2501))
([ecf130d](ecf130d3ac))
* bump rusqlite from 0.32.1 to 0.40.1
([#2287](https://github.com/headroomlabs-ai/headroom/issues/2287))
([522faa1](522faa1a59))
* bump the cargo-minor-patch group across 1 directory with 22 updates
([#2916](https://github.com/headroomlabs-ai/headroom/issues/2916))
([148d860](148d8605e2))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-12 19:02:51 -05:00

378 lines
14 KiB
Python

"""Unit tests for headroom.binaries — the lazy fetcher for bundled CLI tools.
No network access. A fake urlopen serves bytes from an in-memory fixture.
"""
from __future__ import annotations
import hashlib
import io
import sys
import tarfile
import zipfile
import pytest
from headroom import binaries
# -------- Fixtures -------------------------------------------------------- #
@pytest.fixture(autouse=True)
def _clear_caches(monkeypatch, tmp_path):
"""Isolate every test from global state: cache dir, platform lru_cache, env."""
binaries.detect_platform.cache_clear()
binaries._registry.cache_clear()
monkeypatch.setenv("HEADROOM_BINARIES_CACHE", str(tmp_path / "cache"))
monkeypatch.delenv("HEADROOM_BINARIES_MIRROR", raising=False)
monkeypatch.delenv("HEADROOM_BINARIES_OFFLINE", raising=False)
yield
binaries.detect_platform.cache_clear()
binaries._registry.cache_clear()
def _set_platform(monkeypatch, *, sys_plat: str, machine: str, musl: bool = False):
monkeypatch.setattr(sys, "platform", sys_plat)
monkeypatch.setattr("platform.machine", lambda: machine)
monkeypatch.setattr(binaries, "_is_musl", lambda: musl)
binaries.detect_platform.cache_clear()
def _make_tar_gz(files: dict[str, bytes]) -> bytes:
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tf:
for name, data in files.items():
info = tarfile.TarInfo(name=name)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
return buf.getvalue()
def _make_zip(files: dict[str, bytes]) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
for name, data in files.items():
zf.writestr(name, data)
return buf.getvalue()
class _FakeResponse:
def __init__(self, data: bytes):
self._data = data
self.headers = {"Content-Length": str(len(data))}
def read(self, n: int = -1) -> bytes:
if n < 0 or n >= len(self._data):
chunk, self._data = self._data, b""
return chunk
chunk, self._data = self._data[:n], self._data[n:]
return chunk
def __enter__(self):
return self
def __exit__(self, *a):
return False
@pytest.fixture
def fake_urlopen(monkeypatch):
"""Install a fake urllib.request.urlopen that serves registered URLs."""
served: dict[str, bytes] = {}
def fake(req, timeout=None): # noqa: ARG001
url = req.full_url if hasattr(req, "full_url") else req
if url not in served:
raise AssertionError(f"unexpected fetch for {url}")
return _FakeResponse(served[url])
monkeypatch.setattr(binaries.urllib.request, "urlopen", fake)
return served
# -------- Platform detection --------------------------------------------- #
def test_detect_platform_linux_gnu(monkeypatch):
_set_platform(monkeypatch, sys_plat="linux", machine="x86_64", musl=False)
p = binaries.detect_platform()
assert p == binaries.PlatformKey("linux", "x86_64", "gnu")
assert p.key() == "linux-x86_64-gnu"
def test_detect_platform_linux_musl(monkeypatch):
_set_platform(monkeypatch, sys_plat="linux", machine="aarch64", musl=True)
assert binaries.detect_platform().key() == "linux-aarch64-musl"
def test_detect_platform_darwin_arm64(monkeypatch):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
assert binaries.detect_platform().key() == "darwin-aarch64"
def test_detect_platform_windows_amd64(monkeypatch):
_set_platform(monkeypatch, sys_plat="win32", machine="AMD64")
assert binaries.detect_platform().key() == "windows-x86_64"
# -------- Cache dir ------------------------------------------------------ #
def test_cache_dir_respects_env_override(monkeypatch, tmp_path):
monkeypatch.setenv("HEADROOM_BINARIES_CACHE", str(tmp_path / "custom"))
assert binaries.cache_dir() == (tmp_path / "custom").resolve()
# -------- Registry / asset resolution ------------------------------------ #
def test_unsupported_platform_raises(monkeypatch):
_set_platform(monkeypatch, sys_plat="linux", machine="riscv64")
with pytest.raises(binaries.PlatformNotSupported):
binaries._asset_for_platform("difft", binaries.detect_platform())
def test_pypi_only_tool_raises_with_helpful_message(monkeypatch):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
with pytest.raises(binaries.PlatformNotSupported) as exc:
binaries._asset_for_platform("ast-grep", binaries.detect_platform())
assert "pip install headroom-ai" in str(exc.value)
def test_unknown_tool_raises_key_error():
with pytest.raises(KeyError):
binaries._tool_entry("not-a-real-tool")
# -------- which / resolve with PATH hits --------------------------------- #
def test_which_finds_on_path(monkeypatch, tmp_path):
fake_bin = tmp_path / "difft"
fake_bin.write_text("#!/bin/sh\necho ok\n")
fake_bin.chmod(0o755)
monkeypatch.setattr(
binaries.shutil, "which", lambda name: str(fake_bin) if name == "difft" else None
)
# Because the tool is on PATH, which() returns its path without fetching.
assert binaries.which("difft") == fake_bin
def test_which_returns_none_when_not_cached(monkeypatch):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
assert binaries.which("difft") is None
def test_resolve_honors_path(monkeypatch, tmp_path):
fake_bin = tmp_path / "scc"
fake_bin.write_text("")
fake_bin.chmod(0o755)
monkeypatch.setattr(
binaries.shutil, "which", lambda name: str(fake_bin) if name == "scc" else None
)
assert binaries.resolve("scc") == fake_bin
# -------- Offline / mirror / fetch behavior ------------------------------ #
def test_offline_error_when_fetch_required(monkeypatch):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
monkeypatch.setenv("HEADROOM_BINARIES_OFFLINE", "1")
with pytest.raises(binaries.OfflineError):
binaries.resolve("difft")
def test_mirror_substitution(monkeypatch):
monkeypatch.setenv("HEADROOM_BINARIES_MIRROR", "https://mirror.example.com/gh")
out = binaries._mirror_url(
"https://github.com/Wilfred/difftastic/releases/download/0.64.0/x.tar.gz"
)
assert (
out == "https://mirror.example.com/gh/Wilfred/difftastic/releases/download/0.64.0/x.tar.gz"
)
# Non-matching URLs are left alone.
assert binaries._mirror_url("https://example.com/x") == "https://example.com/x"
def test_mirror_url_with_query_params_strips_them_from_download_filename(monkeypatch, fake_urlopen):
"""Mirror URLs with `?token=...` must not leak into the download filename.
Regression test for the case where `Path(url).name` gave
`difft.tar.gz?token=abc`, which broke `.endswith(".tar.gz")` detection
and resulted in a raw archive being copied as an "executable."
"""
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
archive = _make_tar_gz({"difft": b"ok"})
tokened_url = (
"https://github.com/Wilfred/difftastic/releases/download/0.64.0/"
"difft-aarch64-apple-darwin.tar.gz?token=abc&sig=xyz"
)
# Override the registry URL for this test.
reg = binaries._registry()
asset = reg["tools"]["difft"]["assets"]["darwin-aarch64"]
original_url = asset["url"]
asset["url"] = tokened_url
fake_urlopen[tokened_url] = archive
try:
path = binaries.resolve("difft")
assert path.exists()
assert path.read_bytes() == b"ok"
finally:
asset["url"] = original_url
def test_fetch_extract_and_cache_tar_gz(monkeypatch, fake_urlopen, tmp_path):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
payload = b"#!/bin/sh\necho fake-difft\n"
archive = _make_tar_gz({"difft-0.64.0/difft": payload})
url = "https://github.com/Wilfred/difftastic/releases/download/0.64.0/difft-aarch64-apple-darwin.tar.gz"
fake_urlopen[url] = archive
path = binaries.resolve("difft")
assert path.exists()
assert path.read_bytes() == payload
# Second call should use cache (no further fetch).
fake_urlopen.pop(url) # remove so a refetch would error
path2 = binaries.resolve("difft")
assert path2 == path
def test_fetch_extract_zip(monkeypatch, fake_urlopen):
_set_platform(monkeypatch, sys_plat="win32", machine="AMD64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
payload = b"MZfake"
archive = _make_zip({"scc.exe": payload})
url = "https://github.com/boyter/scc/releases/download/v3.5.0/scc_Windows_x86_64.zip"
fake_urlopen[url] = archive
path = binaries.resolve("scc")
assert path.exists()
assert path.name.endswith("scc.exe")
assert path.read_bytes() == payload
def test_download_retries_transient_network_failure(monkeypatch, tmp_path):
attempts = 0
sleeps: list[float] = []
def flaky_urlopen(req, timeout=None): # noqa: ARG001
nonlocal attempts
attempts += 1
if attempts < 3:
import urllib.error
raise urllib.error.URLError("temporary GitHub release failure")
return _FakeResponse(b"binary")
monkeypatch.setattr(binaries.urllib.request, "urlopen", flaky_urlopen)
monkeypatch.setattr(binaries.time, "sleep", sleeps.append)
destination = tmp_path / "download"
binaries._download("https://github.com/example/tool", destination, progress=False)
assert attempts == 3
assert sleeps == [0.25, 0.5]
assert destination.read_bytes() == b"binary"
def test_sha256_mismatch_raises_and_deletes(monkeypatch, fake_urlopen, tmp_path):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
# Override the registry entry for difft to include a bogus sha256.
reg = binaries._registry()
asset = reg["tools"]["difft"]["assets"]["darwin-aarch64"]
asset["sha256"] = "deadbeef" * 8 # wrong
archive = _make_tar_gz({"difft": b"hi"})
fake_urlopen[asset["url"]] = archive
try:
with pytest.raises(binaries.Sha256Mismatch):
binaries.resolve("difft")
finally:
asset["sha256"] = None # restore
def test_sha256_match_passes(monkeypatch, fake_urlopen):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
archive = _make_tar_gz({"difft": b"hello"})
good = hashlib.sha256(archive).hexdigest()
reg = binaries._registry()
asset = reg["tools"]["difft"]["assets"]["darwin-aarch64"]
asset["sha256"] = good
fake_urlopen[asset["url"]] = archive
try:
path = binaries.resolve("difft")
assert path.read_bytes() == b"hello"
finally:
asset["sha256"] = None
# -------- status() ------------------------------------------------------- #
def test_ensure_tools_survives_readonly_cache_dir(monkeypatch, tmp_path):
"""Containerized / read-only home dirs must not crash proxy startup.
Regression test for PermissionError not being caught in ensure_tools().
"""
_set_platform(monkeypatch, sys_plat="linux", machine="x86_64", musl=False)
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
# Point the cache at a path we can't create under (readonly parent).
readonly_parent = tmp_path / "readonly"
readonly_parent.mkdir()
readonly_parent.chmod(0o500) # r-x: can't create children
monkeypatch.setenv("HEADROOM_BINARIES_CACHE", str(readonly_parent / "cache"))
try:
# Must return a dict, not raise.
result = binaries.ensure_tools(quiet=True)
# Fetched tools couldn't write to cache → None. ast-grep is PyPI-only
# so its entry depends on PATH, which is not what this test exercises.
assert result.get("difft") is None
assert result.get("scc") is None
finally:
readonly_parent.chmod(0o700) # so pytest tmp cleanup succeeds
def test_ensure_tools_partial_failure_proxy_still_starts(monkeypatch):
"""If one tool fails to fetch, others still install and ensure_tools returns."""
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
scc_asset = binaries._registry()["tools"]["scc"]["assets"]["darwin-aarch64"]
scc_tar = _make_tar_gz({"scc": b"ok"})
def selective_urlopen(req, timeout=None): # noqa: ARG001
url = req.full_url if hasattr(req, "full_url") else req
if url == scc_asset["url"]:
return _FakeResponse(scc_tar)
# difft fails with a real network-style error.
import urllib.error
raise urllib.error.URLError("simulated network failure")
monkeypatch.setattr(binaries.urllib.request, "urlopen", selective_urlopen)
result = binaries.ensure_tools(quiet=True)
# scc succeeded; difft failed but ensure_tools() did not crash.
assert result["scc"] is not None
assert result["difft"] is None
def test_status_reports_every_registered_tool(monkeypatch):
_set_platform(monkeypatch, sys_plat="darwin", machine="arm64")
monkeypatch.setattr(binaries.shutil, "which", lambda _name: None)
rows = binaries.status()
names = {r["tool"] for r in rows}
assert {"difft", "scc", "ast-grep"} <= names
for r in rows:
assert r["state"] in ("on-path", "cached", "missing", "unsupported-platform")