Files
hermes-agent/tests/pm/test_downloader.py
ethernet f9ec21538f fix(pm): only loopback-origin downloads may redirect to loopback
The https-across-redirects gate accepted any http://127.0.0.1 / localhost
target so test servers work. That let a production https origin redirect
an unpinned model download (sha256 unset by catalog policy) to whatever is
bound on a local port, unverified. Loopback now requires the request to
have started on loopback; no test-only flag needed.
2026-09-21 18:54:55 -04:00

429 lines
17 KiB
Python

"""pm/downloader: resumable, hash-verified, 8-way parallel downloads.
Real downloads against a loopback Range-honoring server — no mocked
stores. Covers the seams agreed in the plan: run() semantics, the
progress(overall_done, overall_total, ranges) contract, parallelism,
resume-refetch-only-missing, pause, and the optional-hash policy.
"""
from __future__ import annotations
import hashlib
import json
import os
import threading
from pathlib import Path
import pytest
from pm.downloader import (Download, DownloadError, DownloadPaused,
HashError, Source, replace_when_released)
from tests.pm._range_server import RangeHandler as _Handler, url as _url
from tests.pm._range_server import dl_server as dl_server
def _payload(n: int, seed: bytes = b"x") -> bytes:
return seed * n
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
# ── parallelism ───────────────────────────────────────────────
@pytest.mark.parametrize("connections", [2, 8])
def test_parallel_requests_overlap_without_exceeding_limit(tmp_path, connections):
from http.server import ThreadingHTTPServer
total = 8 * (4 << 20) # 32 MiB -> 8 x 4 MiB ranges
payload = _payload(total)
barrier = threading.Barrier(connections)
lock = threading.Lock()
active = peak = 0
broken = []
class ConcurrentHandler(_Handler):
payloads = {"/big": payload}
ranges_seen = []
def do_GET(self):
nonlocal active, peak
if self.headers.get("Range") == "bytes=0-0":
return super().do_GET()
with lock:
active += 1
peak = max(peak, active)
try:
try:
barrier.wait(timeout=5)
except threading.BrokenBarrierError:
broken.append(self.headers.get("Range"))
super().do_GET()
finally:
with lock:
active -= 1
dest = tmp_path / "big.bin"
with ThreadingHTTPServer(("127.0.0.1", 0), ConcurrentHandler) as server:
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
Download([Source(_url(server, "/big"), dest, _sha(payload))],
connections=connections, partials_dir=tmp_path / "partials").run()
finally:
server.shutdown()
thread.join(timeout=5)
assert dest.read_bytes() == payload
assert not broken, "range requests were serialized instead of overlapping"
assert peak == connections and active == 0
expected = [(i * total // connections, (i + 1) * total // connections - 1) for i in range(connections)]
assert sorted((s, e) for _, s, e in ConcurrentHandler.ranges_seen) == expected
def test_progress_carries_overall_and_ranges(dl_server, tmp_path):
p1, p2 = _payload(1000, b"x"), _payload(2000, b"y")
_Handler.payloads["/a"] = p1
_Handler.payloads["/b"] = p2
d1, d2 = tmp_path / "a.bin", tmp_path / "b.bin"
dl = Download([
Source(_url(dl_server, "/a"), d1, _sha(p1)),
Source(_url(dl_server, "/b"), d2, _sha(p2)),
], partials_dir=tmp_path / "partials")
seen = []
dl.run(progress=lambda d, t, r: seen.append((d, t, dict(r))))
final_d, final_t, final_r = seen[-1]
assert final_d == 3000
assert final_t == 3000
assert final_r[str(d1)] == [(0, 1000)]
assert final_r[str(d2)] == [(0, 2000)]
# ── optional hash ─────────────────────────────────────────────
def test_hash_mismatch_raises_and_deletes_part(dl_server, tmp_path):
_Handler.payloads["/f"] = _payload(1 << 20)
dest = tmp_path / "f.bin"
partials = tmp_path / "partials"
dl = Download([Source(_url(dl_server, "/f"), dest, "0" * 64)],
partials_dir=partials)
with pytest.raises(HashError):
dl.run()
assert not dest.exists()
assert not [path for path in partials.iterdir() if path.name != ".locks"] # .part + .ranges both deleted
def test_no_hash_accepts_any_bytes_of_right_size(dl_server, tmp_path):
payload = _payload(1 << 20)
_Handler.payloads["/f"] = payload
dest = tmp_path / "f.bin"
dl = Download([Source(_url(dl_server, "/f"), dest)],
partials_dir=tmp_path / "partials")
moved = dl.run()
assert moved == [dest]
assert dest.read_bytes() == payload
# ── resume ────────────────────────────────────────────────────
def test_partials_never_in_scratch_or_dest(dl_server, tmp_path):
payload = _payload(1 << 20)
_Handler.payloads["/p"] = payload
dest = tmp_path / "p.bin"
partials = tmp_path / "partials"
dl = Download([Source(_url(dl_server, "/p"), dest)],
partials_dir=partials)
dl.run()
# dest is the only file in its dir; partials dir is empty after mv.
# (The test harness may drop its own marker dir into tmp_path.)
assert dest.exists()
assert not [path for path in partials.iterdir() if path.name != ".locks"]
assert not [p for p in tmp_path.iterdir()
if p.suffix in (".part", ".ranges")]
@pytest.mark.parametrize("hashed", [False, True])
def test_completed_dest_is_skipped(dl_server, tmp_path, hashed):
payload = _payload(1 << 20)
_Handler.payloads["/s"] = payload
dest = tmp_path / "s.bin"
dest.write_bytes(payload)
dl = Download([Source(_url(dl_server, "/s"), dest, _sha(payload) if hashed else None)],
partials_dir=tmp_path / "partials")
dl.run()
assert _Handler.ranges_seen == [] # nothing fetched
def test_stale_dest_with_wrong_hash_is_refetched(dl_server, tmp_path):
payload = _payload(1 << 20)
_Handler.payloads["/s"] = payload
dest = tmp_path / "s.bin"
dest.write_bytes(_payload(1 << 20, seed=b"wrong")) # wrong bytes on disk
dl = Download([Source(_url(dl_server, "/s"), dest, _sha(payload))],
partials_dir=tmp_path / "partials")
dl.run()
assert dest.read_bytes() == payload # stale dest replaced
assert _Handler.ranges_seen # a fetch actually happened
def test_redirect_to_non_https_refused():
import urllib.request
from pm.downloader import _HttpsRedirectHandler
handler = _HttpsRedirectHandler()
req = urllib.request.Request("https://example.com/a")
with pytest.raises(DownloadError):
handler.redirect_request(req, None, 302, "Found", {},
"http://example.com/b")
# An https redirect resolves through the default handler.
assert handler.redirect_request(req, None, 302, "Found", {},
"https://example.com/b") is not None
# Loopback is a test-server affordance: an https origin may not land there.
with pytest.raises(DownloadError):
handler.redirect_request(req, None, 302, "Found", {}, "http://127.0.0.1:8000/b")
local = urllib.request.Request("http://127.0.0.1:8000/a")
assert handler.redirect_request(local, None, 302, "Found", {},
"http://localhost:8000/b") is not None
# ── pause ─────────────────────────────────────────────────────
# ── safety ────────────────────────────────────────────────────
def test_refuses_non_https_non_loopback(tmp_path):
dl = Download([Source("http://example.com/x", tmp_path / "x.bin")],
partials_dir=tmp_path / "partials")
with pytest.raises(ValueError):
dl.run()
# ── edge cases: no-Range fallback, multi-source resume, pause mid-plan ──
def test_no_range_fallback_downloads_full_body(dl_server, tmp_path):
"""A server that ignores Range is served by the single-stream fallback:
the whole body arrives and progress reports the full covered range."""
_Handler.no_range = True
payload = _payload(1 << 20)
_Handler.payloads["/nr"] = payload
dest = tmp_path / "nr.bin"
dl = Download([Source(_url(dl_server, "/nr"), dest, _sha(payload))],
partials_dir=tmp_path / "partials")
seen = []
moved = dl.run(progress=lambda d, t, r: seen.append((d, t, dict(r))))
assert moved == [dest]
assert dest.read_bytes() == payload
assert _Handler.ranges_seen == [] # never used Range
d, t, r = seen[-1]
assert d == t == len(payload)
assert r[str(dest)] == [(0, len(payload))]
def test_no_range_short_body_raises_and_leaves_no_dest(dl_server, tmp_path):
"""The fallback errors when the server sends FEWER bytes than its
declared Content-Length (connection dropped mid-body), leaving the
partial in the managed area and NO dest."""
_Handler.no_range = True
payload = _payload(2 << 20)
_Handler.payloads["/ns"] = payload
_Handler.abort_after = 1 << 20 # server declares 2 MiB, sends 1 MiB
dest = tmp_path / "ns.bin"
partials = tmp_path / "partials"
dl = Download([Source(_url(dl_server, "/ns"), dest)],
partials_dir=partials)
with pytest.raises(DownloadError):
dl.run()
assert not dest.exists()
names = {p.name for p in partials.iterdir()}
assert any(n.endswith(".part") for n in names)
assert any(n.endswith(".ranges") for n in names)
def test_resume_across_plan_skips_completed_source(dl_server, tmp_path):
"""A 2-source plan: source 1 completes, source 2 aborts. A second run
of the SAME plan completes BOTH files without re-fetching source 1 —
source 1 gets exactly one request across both runs and source 2's
second request starts at its abort point."""
total_b = 8 << 20
p_a, p_b = _payload(1 << 20, b"a"), _payload(total_b, b"b")
_Handler.payloads["/a"] = p_a
_Handler.payloads["/b"] = p_b
da, db = tmp_path / "a.bin", tmp_path / "b.bin"
partials = tmp_path / "partials"
_Handler.abort_after = 2 << 20
dl = Download([Source(_url(dl_server, "/a"), da, _sha(p_a)),
Source(_url(dl_server, "/b"), db, _sha(p_b))],
partials_dir=partials, connections=1)
with pytest.raises(Exception):
dl.run()
sidecars = list(partials.glob("*.ranges"))
assert len(sidecars) == 1
assert json.loads(sidecars[0].read_text())["ranges"] == [[0, 2 << 20]]
assert da.read_bytes() == p_a and not db.exists()
_Handler.abort_after = None
dl = Download([Source(_url(dl_server, "/a"), da, _sha(p_a)),
Source(_url(dl_server, "/b"), db, _sha(p_b))],
partials_dir=partials, connections=1)
ticks = []
dl.run(progress=lambda d, t, r: ticks.append((d, t, dict(r))))
assert ticks[0] == (len(p_a) + (2 << 20), len(p_a) + len(p_b),
{str(da): [(0, len(p_a))], str(db): [(0, 2 << 20)]})
assert not list(partials.glob("*.ranges")) and not list(partials.glob("*.part"))
assert da.read_bytes() == p_a
assert db.read_bytes() == p_b
a_reqs = [r for r in _Handler.ranges_seen if r[0] == "/a"]
assert len(a_reqs) == 1 # exactly one request across both runs
b_reqs = [r for r in _Handler.ranges_seen if r[0] == "/b"]
# first request starts at 0; second (resume) starts at the abort point
assert b_reqs[0][1] == 0
assert b_reqs[1][1] == 2 << 20
assert b_reqs[1][1] != b_reqs[0][1]
def test_pause_mid_plan_resumes_to_completion(dl_server, tmp_path):
"""Pause after source 1 completes: DownloadPaused is raised, source 1's
dest is moved/complete, and source 2's partial survives in the managed
area; a FRESH Download over the same plan resumes to full completion."""
p_a, p_b = _payload(1 << 20, b"a"), _payload(32 << 20, b"b")
_Handler.payloads["/a"] = p_a
_Handler.payloads["/b"] = p_b
da, db = tmp_path / "a.bin", tmp_path / "b.bin"
partials = tmp_path / "partials"
dl = Download([Source(_url(dl_server, "/a"), da, _sha(p_a)),
Source(_url(dl_server, "/b"), db, _sha(p_b))],
partials_dir=partials)
def pause_second_source(done, total, ranges):
if ranges.get(str(db)):
dl.pause()
with pytest.raises(DownloadPaused):
dl.run(progress=pause_second_source)
assert da.read_bytes() == p_a # source 1 moved despite the pause
assert not db.exists()
names = {p.name for p in partials.iterdir()}
assert any(n.endswith(".part") for n in names)
assert any(n.endswith(".ranges") for n in names)
# a FRESH Download over the same plan resumes to full completion
_Handler.slow_per_chunk = 0.0
dl = Download([Source(_url(dl_server, "/a"), da, _sha(p_a)),
Source(_url(dl_server, "/b"), db, _sha(p_b))],
partials_dir=partials)
dl.run()
assert da.read_bytes() == p_a
assert db.read_bytes() == p_b
# ── publication: a finished file the OS still holds ──────────
def _hold_first_two(replacements: list):
"""An ``os.replace`` that refuses the first two publications of a download's staged file with
the permission error a Windows antivirus or indexing scan raises while it still has the
finished file open. Sidecar writes publish through the same call, so the refusal is keyed to
the downloader's staging suffix rather than to call order."""
real_replace = os.replace
def held(src, dst):
if str(src).endswith(".download") and len(replacements) < 2:
replacements.append(src)
raise PermissionError(13, "Access is denied")
real_replace(src, dst)
return held
def test_replace_waits_out_a_transient_hold(tmp_path, monkeypatch):
tmp = tmp_path / "model.download"
dest = tmp_path / "model.gguf"
tmp.write_bytes(b"weights")
refusals = []
monkeypatch.setattr(os, "replace", _hold_first_two(refusals))
replace_when_released(tmp, dest, timeout=5)
assert len(refusals) == 2
assert dest.read_bytes() == b"weights"
assert not tmp.exists()
def test_replace_gives_up_with_a_plain_language_error(tmp_path, monkeypatch):
"""A hold that outlasts the window is reported as a hold — chained to the OS error, and
never degraded to a copy of the file."""
tmp = tmp_path / "model.download"
dest = tmp_path / "model.gguf"
tmp.write_bytes(b"weights")
def always_held(src, dst):
raise PermissionError(13, "Access is denied")
monkeypatch.setattr(os, "replace", always_held)
with pytest.raises(RuntimeError) as failed:
replace_when_released(tmp, dest, timeout=0.3)
assert "model.download" in str(failed.value)
assert "try again" in str(failed.value).lower()
assert isinstance(failed.value.__cause__, PermissionError)
assert not dest.exists()
assert tmp.read_bytes() == b"weights" # the finished bytes survive for the retry
def test_download_publishes_through_a_held_finished_file(dl_server, tmp_path, monkeypatch):
"""The publish the OS refuses twice must still land the complete file, and the download
must report success — this is the 22 GB model case, not a partial transfer."""
body = _payload(4 << 20, b"m")
_Handler.payloads["/held"] = body
dest = tmp_path / "held.gguf"
refusals = []
monkeypatch.setattr(os, "replace", _hold_first_two(refusals))
dl = Download([Source(_url(dl_server, "/held"), dest, _sha(body))],
partials_dir=tmp_path / "partials")
dl.run()
assert len(refusals) == 2
assert dest.read_bytes() == body
assert not list(tmp_path.glob("*.download"))
assert not [p for p in (tmp_path / "partials").iterdir() if p.suffix in (".part", ".ranges")]
def test_download_reports_a_hold_that_never_releases(dl_server, tmp_path, monkeypatch):
"""A stuck hold surfaces the rename error, not the failed cleanup of the partial it could
not remove either."""
body = _payload(1 << 20, b"h")
_Handler.payloads["/stuck"] = body
dest = tmp_path / "stuck.gguf"
partials = tmp_path / "partials"
def never_released(staged, target, **kw):
raise RuntimeError(f"{staged.name} could not be renamed into place")
real_unlink = Path.unlink
def stuck_partial(self, missing_ok=False):
if self.suffix in (".part", ".ranges"):
raise PermissionError(13, "Access is denied")
return real_unlink(self, missing_ok=missing_ok)
monkeypatch.setattr("pm.downloader.replace_when_released", never_released)
monkeypatch.setattr(Path, "unlink", stuck_partial)
dl = Download([Source(_url(dl_server, "/stuck"), dest, _sha(body))],
partials_dir=partials)
with pytest.raises(RuntimeError, match="could not be renamed into place"):
dl.run()
assert not dest.exists()