124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
"""Recommendation requests reuse disk entries and negotiate gzip across real processes."""
|
|
import gzip
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def portal():
|
|
requests = []
|
|
state = {"version": 1, "status": 200, "compress": True}
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
requests.append(self.headers.get("Accept-Encoding"))
|
|
body = json.dumps({"paidRecommendedModels": [{"modelName": f"model-{state['version']}"}]}).encode()
|
|
compressed = state["compress"] and requests[-1] == "gzip"
|
|
if compressed:
|
|
body = gzip.compress(body)
|
|
self.send_response(state["status"])
|
|
self.send_header("Content-Type", "application/json")
|
|
if compressed:
|
|
self.send_header("Content-Encoding", "gzip")
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *_args):
|
|
pass
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
yield f"http://127.0.0.1:{server.server_port}", requests, state
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
|
|
def fetch_in_new_process(home, base, *, force=False):
|
|
result = subprocess.run(
|
|
[sys.executable, "-c", "from hermes_cli.models import fetch_nous_recommended_models; "
|
|
"import json, sys; print(json.dumps(fetch_nous_recommended_models(sys.argv[1], force_refresh=sys.argv[2] == 'True')))",
|
|
base, str(force)],
|
|
cwd=Path(__file__).resolve().parents[2],
|
|
env={**os.environ, "HERMES_HOME": str(home)},
|
|
capture_output=True, text=True, check=True, timeout=30,
|
|
)
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
def test_process_restarts_reuse_fresh_disk_but_force_and_expiry_fetch(tmp_path, portal):
|
|
base, requests, state = portal
|
|
home = tmp_path / "client"
|
|
first = fetch_in_new_process(home, base)
|
|
assert fetch_in_new_process(home, base) == first
|
|
assert requests == ["gzip"]
|
|
|
|
# A server may ignore Accept-Encoding; the refresh must also accept plain JSON.
|
|
state.update(version=2, compress=False)
|
|
second = fetch_in_new_process(home, base, force=True)
|
|
assert second and second != first
|
|
assert len(requests) == 2
|
|
|
|
path = home / "cache" / "nous_recommended_cache.json"
|
|
disk = json.loads(path.read_text(encoding="utf-8"))
|
|
disk[base]["ts"] = time.time() - 3600
|
|
path.write_text(json.dumps(disk), encoding="utf-8")
|
|
state["version"] = 3
|
|
assert fetch_in_new_process(home, base) != second
|
|
assert len(requests) == 3
|
|
|
|
# A failed refresh retains the last good payload without making it fresh on disk.
|
|
disk = json.loads(path.read_text(encoding="utf-8"))
|
|
disk[base]["ts"] = time.time() - 3600
|
|
path.write_text(json.dumps(disk), encoding="utf-8")
|
|
state["status"] = 503
|
|
assert fetch_in_new_process(home, base) == disk[base]["data"]
|
|
assert json.loads(path.read_text(encoding="utf-8"))[base]["ts"] == disk[base]["ts"]
|
|
|
|
# Bad local cache bytes/metadata must never stop a healthy API refresh.
|
|
state["status"] = 200
|
|
path.write_bytes(b"\xff")
|
|
assert fetch_in_new_process(home, base) == disk[base]["data"]
|
|
for timestamp in [None, True, "invalid", float("nan"), time.time() + 3600, 10 ** 400]:
|
|
disk[base]["ts"] = timestamp
|
|
path.write_text(json.dumps(disk), encoding="utf-8")
|
|
before = len(requests)
|
|
assert fetch_in_new_process(home, base) == disk[base]["data"]
|
|
assert len(requests) == before + 1
|
|
|
|
|
|
def test_disk_cache_is_scoped_to_home_and_portal(tmp_path, portal):
|
|
base, requests, state = portal
|
|
a, b = tmp_path / "a", tmp_path / "b"
|
|
first = fetch_in_new_process(a, base)
|
|
state["version"] = 2
|
|
assert fetch_in_new_process(b, base) != first
|
|
assert fetch_in_new_process(a, base) == first
|
|
assert len(requests) == 2
|
|
assert fetch_in_new_process(a, base + "/staging") != first
|
|
assert len(requests) == 3
|
|
|
|
# The same process can switch profiles under the multiplex gateway too.
|
|
from hermes_cli.models import fetch_nous_recommended_models, _nous_recommended_cache
|
|
from hermes_constants import set_hermes_home_override, reset_hermes_home_override
|
|
_nous_recommended_cache.clear()
|
|
for home, expected in [(a, first), (b, {"paidRecommendedModels": [{"modelName": "model-2"}]}), (a, first)]:
|
|
token = set_hermes_home_override(home)
|
|
try:
|
|
assert fetch_nous_recommended_models(base) == expected
|
|
finally:
|
|
reset_hermes_home_override(token)
|
|
assert len(requests) == 3
|
|
_nous_recommended_cache.clear()
|