mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
🤖 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 <<ccr:...>> 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 <memory> 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 <system-reminder> 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>
378 lines
14 KiB
Python
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")
|