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,81 @@
|
||||
"""Endpoint/token discovery — isolated from the developer's real environment."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from lean_ctx import discovery
|
||||
|
||||
_ENV_KEYS = (
|
||||
"LEAN_CTX_PROXY_URL",
|
||||
"LEAN_CTX_PROXY_PORT",
|
||||
"LEAN_CTX_PROXY_TOKEN",
|
||||
"LEAN_CTX_DATA_DIR",
|
||||
"XDG_DATA_HOME",
|
||||
"XDG_CONFIG_HOME",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated(tmp_path, monkeypatch):
|
||||
"""Clear every discovery env var and root HOME at an empty tmp dir."""
|
||||
for key in _ENV_KEYS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_base_url_explicit_strips_trailing_slash():
|
||||
assert discovery.resolve_base_url("http://host:9/") == "http://host:9"
|
||||
|
||||
|
||||
def test_base_url_from_env(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_PROXY_URL", "http://h:1234/")
|
||||
assert discovery.resolve_base_url() == "http://h:1234"
|
||||
|
||||
|
||||
def test_base_url_defaults_to_loopback(isolated, monkeypatch):
|
||||
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
|
||||
assert discovery.resolve_base_url() == "http://127.0.0.1:4444"
|
||||
|
||||
|
||||
def test_port_env_wins(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_PROXY_PORT", "5005")
|
||||
assert discovery.resolve_port() == 5005
|
||||
|
||||
|
||||
def test_uid_port_matches_rust_formula(isolated, monkeypatch):
|
||||
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
|
||||
assert discovery._uid_port() == 4444
|
||||
monkeypatch.setattr(os, "getuid", lambda: 2999, raising=False)
|
||||
assert discovery._uid_port() == 5443
|
||||
monkeypatch.setattr(os, "getuid", lambda: 500, raising=False)
|
||||
assert discovery._uid_port() == 4444
|
||||
|
||||
|
||||
def test_port_from_config_toml(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
|
||||
(isolated / "config.toml").write_text("proxy_port = 4500\n", encoding="utf-8")
|
||||
assert discovery.resolve_port() == 4500
|
||||
|
||||
|
||||
def test_commented_proxy_port_is_ignored(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
|
||||
(isolated / "config.toml").write_text("# proxy_port = 3128\n", encoding="utf-8")
|
||||
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
|
||||
assert discovery.resolve_port() == 4444
|
||||
|
||||
|
||||
def test_token_env_precedence(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_PROXY_TOKEN", "envtok")
|
||||
assert discovery.resolve_token() == "envtok"
|
||||
|
||||
|
||||
def test_token_from_session_file(isolated, monkeypatch):
|
||||
monkeypatch.setenv("LEAN_CTX_DATA_DIR", str(isolated))
|
||||
(isolated / "session_token").write_text("deadbeef\n", encoding="utf-8")
|
||||
assert discovery.resolve_token() == "deadbeef"
|
||||
|
||||
|
||||
def test_token_absent_returns_none(isolated):
|
||||
assert discovery.resolve_token() is None
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Tests for the LangChain compress integration.
|
||||
|
||||
The content-reattachment logic is unit-tested with a fake message (no LangChain
|
||||
needed); the full ``compress_messages`` path runs against a loopback server when
|
||||
``langchain-core`` is installed.
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
from lean_ctx.langchain import _reattach_content, compress_messages
|
||||
|
||||
|
||||
class _FakeMessage:
|
||||
"""Minimal pydantic-v2-like message: ``content`` plus ``model_copy``."""
|
||||
|
||||
def __init__(self, content, role="user"):
|
||||
self.content = content
|
||||
self.role = role
|
||||
|
||||
def model_copy(self, update):
|
||||
clone = _FakeMessage(self.content, self.role)
|
||||
for key, value in update.items():
|
||||
setattr(clone, key, value)
|
||||
return clone
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, _FakeMessage)
|
||||
and other.content == self.content
|
||||
and other.role == self.role
|
||||
)
|
||||
|
||||
|
||||
def test_reattach_content_swaps_only_content():
|
||||
originals = [_FakeMessage("long original body", "user")]
|
||||
out = _reattach_content(originals, [{"role": "user", "content": "short"}])
|
||||
assert out[0].content == "short"
|
||||
assert out[0].role == "user"
|
||||
assert out[0] is not originals[0]
|
||||
|
||||
|
||||
def test_reattach_content_preserves_originals_on_count_mismatch():
|
||||
originals = [_FakeMessage("a"), _FakeMessage("b")]
|
||||
out = _reattach_content(originals, [{"content": "x"}])
|
||||
assert out == originals
|
||||
|
||||
|
||||
def test_reattach_content_keeps_message_without_model_copy():
|
||||
class _Plain:
|
||||
content = "untouched"
|
||||
|
||||
originals = [_Plain()]
|
||||
out = _reattach_content(originals, [{"content": "new"}])
|
||||
assert out[0].content == "untouched"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_url():
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self): # noqa: N802
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
out = []
|
||||
for message in body["messages"]:
|
||||
rewritten = dict(message)
|
||||
if isinstance(rewritten.get("content"), str):
|
||||
rewritten["content"] = rewritten["content"][:8]
|
||||
out.append(rewritten)
|
||||
payload = json.dumps({"messages": out, "stats": {}}).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
host, port = httpd.server_address
|
||||
yield f"http://{host}:{port}"
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def test_compress_messages_rewrites_content(base_url):
|
||||
pytest.importorskip("langchain_core")
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
messages = [
|
||||
SystemMessage(content="You are a helpful assistant."),
|
||||
HumanMessage(content="this is a long message body"),
|
||||
]
|
||||
out = compress_messages(messages, model="gpt-4o", base_url=base_url)
|
||||
assert type(out[1]).__name__ == "HumanMessage"
|
||||
assert out[1].content == "this is "
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Tests for the LiteLLM integration.
|
||||
|
||||
``compress_request_data`` is exercised end-to-end against a loopback server;
|
||||
the ``LeanCtxLiteLLMHandler`` is tested via its import guard (and its async hook
|
||||
when LiteLLM happens to be installed).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
from lean_ctx.errors import LeanCtxConnectionError
|
||||
from lean_ctx.litellm import (
|
||||
_LITELLM_AVAILABLE,
|
||||
LeanCtxLiteLLMHandler,
|
||||
compress_request_data,
|
||||
)
|
||||
from lean_ctx.proxy import ProxyClient
|
||||
|
||||
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self): # noqa: N802 (BaseHTTPRequestHandler API)
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
out = []
|
||||
for message in body["messages"]:
|
||||
rewritten = dict(message)
|
||||
if isinstance(rewritten.get("content"), str):
|
||||
rewritten["content"] = rewritten["content"][:8]
|
||||
out.append(rewritten)
|
||||
payload = json.dumps({"messages": out, "stats": {}}).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def log_message(self, *args): # silence test output
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_url():
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
host, port = httpd.server_address
|
||||
yield f"http://{host}:{port}"
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def test_compress_request_data_rewrites_messages_in_place(base_url):
|
||||
client = ProxyClient(base_url=base_url)
|
||||
data = {"model": "gpt-4o", "messages": [{"role": "user", "content": "a long message body"}]}
|
||||
out = compress_request_data(data, client=client)
|
||||
assert out is data
|
||||
assert data["messages"] == [{"role": "user", "content": "a long m"}]
|
||||
|
||||
|
||||
def test_compress_request_data_ignores_empty_messages():
|
||||
assert compress_request_data({"messages": []}) == {"messages": []}
|
||||
assert compress_request_data({}) == {}
|
||||
|
||||
|
||||
def test_compress_request_data_passthrough_when_proxy_down():
|
||||
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
|
||||
data = {"messages": [{"role": "user", "content": "x" * 40}]}
|
||||
out = compress_request_data(data, client=client)
|
||||
assert out["messages"][0]["content"] == "x" * 40
|
||||
|
||||
|
||||
def test_compress_request_data_raise_on_error():
|
||||
client = ProxyClient(base_url="http://127.0.0.1:1", token="t")
|
||||
data = {"messages": [{"role": "user", "content": "x" * 40}]}
|
||||
with pytest.raises(LeanCtxConnectionError):
|
||||
compress_request_data(data, client=client, raise_on_error=True)
|
||||
|
||||
|
||||
def test_handler_requires_litellm():
|
||||
if _LITELLM_AVAILABLE:
|
||||
pytest.skip("litellm installed; the ImportError guard is not exercised")
|
||||
with pytest.raises(ImportError):
|
||||
LeanCtxLiteLLMHandler()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _LITELLM_AVAILABLE, reason="litellm not installed")
|
||||
def test_async_pre_call_hook_compresses(base_url):
|
||||
handler = LeanCtxLiteLLMHandler(base_url=base_url)
|
||||
data = {"messages": [{"role": "user", "content": "a long message body"}]}
|
||||
out = asyncio.run(handler.async_pre_call_hook(None, None, data, "completion"))
|
||||
assert out["messages"][0]["content"] == "a long m"
|
||||
@@ -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