Files
hermes-agent/tests/pm/_range_server.py
ethernet 3d12e86ef1 feat(pm): unified package manager — pm store foundation
Introduce the pm store: a unified, hash-verified package store that
replaces lazy_deps and the old installer's ad-hoc tool downloads.
Store tools are provisioned on PATH (ffmpeg, node/npm via pinned uv),
with a resumable 8-way downloader, verify() returning failure reasons,
and adopt() made EPERM-safe. chromium ships in the payload for every
target. The 3600-line install.sh is replaced by a staged bootstrapper
(heavy deps are pm's job after this); setup-hermes.sh, Dockerfile and
nix pin tables are rewired onto the store. Old install-script tests,
lazy_deps/managed_uv/build_info, and the ps1/bash installer test
batteries are removed with the machinery they tested.

Rebuilt from ethie/pm onto upstream/main (ac6c8028e0) after the
utf-8-sig sweep. 16 hot files (main also churned them) hand-merged:
platform adapters, main.py, electron/main.ts, tui_gateway/server.py,
cua_backend, installer-tests workflow, install.sh (full rewrite),
setup-hermes.sh, plugins doc.
2026-08-31 18:00:48 -04:00

113 lines
4.1 KiB
Python

"""Shared Range-honoring loopback server for pm downloader tests.
Both test_downloader.py and test_store_resume.py exercise real downloads
through this one server instead of each re-implementing it. Behaviour is
configured with class attributes on :class:`RangeHandler`; the ``dl_server``
fixture starts one and resets its state.
"""
from __future__ import annotations
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
class RangeHandler(BaseHTTPRequestHandler):
payloads: dict = {}
ranges_seen: list = [] # (path, start, end) from real Range requests
abort_after: int | None = None # close the connection after this many bytes
slow_per_chunk: float = 0.0 # sleep per served piece (pause tests)
no_range: bool = False # ignore Range, serve 200 full body
chunk: int = 1 << 20 # serve piece size
def log_message(self, *args): # noqa: A002 - silence request logging
pass
def do_GET(self): # noqa: N802 - http.server API
payload = self.payloads.get(self.path)
if payload is None:
self.send_error(404)
return
if self.no_range:
# A server that ignores Range: 200 with the full body, even
# when the client asked for a byte range.
self.send_response(200)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
served = 0
while served < len(payload):
if self.abort_after is not None and served >= self.abort_after:
self.connection.close()
return
if self.slow_per_chunk:
time.sleep(self.slow_per_chunk)
piece = payload[served:served + self.chunk]
self.wfile.write(piece)
self.wfile.flush()
served += len(piece)
return
rng = self.headers.get("Range")
if rng:
spec = rng.removeprefix("bytes=")
if spec == "0-0":
# probe: 206 with total from Content-Range, 1 byte body
total = len(payload)
self.send_response(206)
self.send_header("Content-Range", f"bytes 0-{total - 1}/{total}")
self.send_header("Content-Length", "1")
self.end_headers()
self.wfile.write(payload[:1])
self.wfile.flush()
return
start_s, end_s = spec.split("-", 1)
start, end = int(start_s), int(end_s)
self.ranges_seen.append((self.path, start, end))
body = payload[start:end + 1]
self.send_response(206)
self.send_header("Content-Range",
f"bytes {start}-{end}/{len(payload)}")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
served = 0
while served < len(body):
if self.abort_after is not None and served >= self.abort_after:
self.connection.close()
return
if self.slow_per_chunk:
time.sleep(self.slow_per_chunk)
piece = body[served:served + self.chunk]
self.wfile.write(piece)
self.wfile.flush()
served += len(piece)
return
self.send_response(200)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
self.wfile.flush()
@pytest.fixture
def dl_server():
RangeHandler.payloads = {}
RangeHandler.ranges_seen = []
RangeHandler.abort_after = None
RangeHandler.slow_per_chunk = 0.0
RangeHandler.no_range = False
RangeHandler.chunk = 1 << 20
server = HTTPServer(("127.0.0.1", 0), RangeHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield server
finally:
server.shutdown()
server.server_close()
def url(server, path: str) -> str:
return f"http://127.0.0.1:{server.server_port}{path}"