chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""Transport tests for ProxyClient against a real loopback HTTP server.
|
||||
|
||||
The server is a faithful stand-in for the daemon's ``/v1/compress`` contract
|
||||
(echoes the request, returns a stats block); it exercises request building,
|
||||
auth headers, response parsing and error mapping — the compression itself is
|
||||
covered by the Rust unit tests.
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
from lean_ctx import ProxyClient, compress
|
||||
from lean_ctx.errors import LeanCtxAuthError, LeanCtxConnectionError, LeanCtxError
|
||||
|
||||
EXPECTED_TOKEN = "secret-token"
|
||||
|
||||
|
||||
class _CompressHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self): # noqa: N802 (BaseHTTPRequestHandler API)
|
||||
if self.path != "/v1/compress":
|
||||
self.send_error(404)
|
||||
return
|
||||
auth = self.headers.get("Authorization", "")
|
||||
if self.server.require_token and auth != f"Bearer {EXPECTED_TOKEN}":
|
||||
self._json(401, {"error": "unauthorized"})
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
self.server.last_request = {"headers": dict(self.headers), "body": body}
|
||||
|
||||
original = 0
|
||||
compressed = 0
|
||||
out = []
|
||||
for message in body["messages"]:
|
||||
rewritten = dict(message)
|
||||
content = rewritten.get("content")
|
||||
if isinstance(content, str):
|
||||
original += len(content)
|
||||
rewritten["content"] = content[:8]
|
||||
compressed += len(rewritten["content"])
|
||||
out.append(rewritten)
|
||||
|
||||
saved = original - compressed
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"messages": out,
|
||||
"stats": {
|
||||
"original_tokens": original,
|
||||
"compressed_tokens": compressed,
|
||||
"saved_tokens": saved,
|
||||
"saved_pct": round(saved / original * 100, 1) if original else 0.0,
|
||||
"model": body.get("model"),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def do_GET(self): # noqa: N802 (BaseHTTPRequestHandler API)
|
||||
if not self.path.startswith("/v1/references/"):
|
||||
self.send_error(404)
|
||||
return
|
||||
reference_id = self.path.rsplit("/", 1)[-1]
|
||||
if reference_id == "missing":
|
||||
self._text(404, "Reference expired or not found")
|
||||
return
|
||||
self._text(200, f"ORIGINAL[{reference_id}]")
|
||||
|
||||
def _text(self, status, text):
|
||||
data = text.encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def _json(self, status, payload):
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def log_message(self, *args): # silence test output
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def server():
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _CompressHandler)
|
||||
httpd.require_token = False
|
||||
httpd.last_request = None
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
host, port = httpd.server_address
|
||||
yield httpd, f"http://{host}:{port}"
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def test_compress_function_returns_messages(server):
|
||||
httpd, base_url = server
|
||||
out = compress(
|
||||
[{"role": "user", "content": "this is a long message body"}],
|
||||
model="gpt-4o",
|
||||
base_url=base_url,
|
||||
token=EXPECTED_TOKEN,
|
||||
)
|
||||
assert out == [{"role": "user", "content": "this is "}]
|
||||
|
||||
|
||||
def test_client_returns_stats_and_sends_model(server):
|
||||
httpd, base_url = server
|
||||
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
|
||||
result = client.compress(
|
||||
[{"role": "user", "content": "abcdefghijklmnop"}],
|
||||
model="claude-sonnet-4",
|
||||
)
|
||||
assert result.saved_tokens == len("abcdefghijklmnop") - 8
|
||||
assert result.saved_pct > 0
|
||||
assert httpd.last_request["body"]["model"] == "claude-sonnet-4"
|
||||
assert httpd.last_request["headers"]["Content-Type"] == "application/json"
|
||||
|
||||
|
||||
def test_auth_error_maps_to_exception(server):
|
||||
httpd, base_url = server
|
||||
httpd.require_token = True
|
||||
with pytest.raises(LeanCtxAuthError):
|
||||
compress(
|
||||
[{"role": "user", "content": "needs a valid token here"}],
|
||||
base_url=base_url,
|
||||
token="wrong",
|
||||
)
|
||||
|
||||
|
||||
def test_connection_error_when_daemon_down():
|
||||
# Port 1 is never an open lean-ctx proxy → URLError → ConnectionError.
|
||||
with pytest.raises(LeanCtxConnectionError):
|
||||
compress(
|
||||
[{"role": "user", "content": "x" * 50}],
|
||||
base_url="http://127.0.0.1:1",
|
||||
token="t",
|
||||
)
|
||||
|
||||
|
||||
def test_non_list_messages_rejected():
|
||||
with pytest.raises(TypeError):
|
||||
ProxyClient(base_url="http://127.0.0.1:1", token="t").compress(
|
||||
{"role": "user", "content": "not a list"}
|
||||
)
|
||||
|
||||
|
||||
def test_malformed_response_raises(server):
|
||||
httpd, base_url = server
|
||||
|
||||
class _Bad(_CompressHandler):
|
||||
def do_POST(self): # noqa: N802
|
||||
self._json(200, {"unexpected": True})
|
||||
|
||||
httpd.RequestHandlerClass = _Bad
|
||||
with pytest.raises(LeanCtxError):
|
||||
compress(
|
||||
[{"role": "user", "content": "y" * 40}],
|
||||
base_url=base_url,
|
||||
token=EXPECTED_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_reference_returns_content(server):
|
||||
httpd, base_url = server
|
||||
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
|
||||
assert client.resolve_reference("abc123") == "ORIGINAL[abc123]"
|
||||
|
||||
|
||||
def test_resolve_reference_missing_raises(server):
|
||||
httpd, base_url = server
|
||||
client = ProxyClient(base_url=base_url, token=EXPECTED_TOKEN)
|
||||
with pytest.raises(LeanCtxError):
|
||||
client.resolve_reference("missing")
|
||||
|
||||
|
||||
def test_resolve_reference_empty_id_rejected():
|
||||
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
|
||||
with pytest.raises(ValueError):
|
||||
client.resolve_reference("")
|
||||
Reference in New Issue
Block a user