Files
yvgude--lean-ctx/rust/tests/embedding_download_test.py
wehub-resource-sync 26382a7ac6
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
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

301 lines
11 KiB
Python

#!/usr/bin/env python3
"""Test embedding model download and hybrid search end-to-end."""
import json
import os
import subprocess
import sys
import tempfile
import time
BINARY = os.path.join(os.path.dirname(__file__), "..", "target", "release", "lean-ctx")
PASS = 0
FAIL = 0
class McpClient:
def __init__(self, binary, cwd):
self.proc = subprocess.Popen(
[binary],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=cwd,
bufsize=0,
)
time.sleep(0.3)
def send(self, obj):
line = json.dumps(obj).encode() + b"\n"
self.proc.stdin.write(line)
self.proc.stdin.flush()
def recv(self, timeout=60):
import select as sel
fd = self.proc.stdout.fileno()
deadline = time.time() + timeout
buf = b""
while time.time() < deadline:
remaining = max(0.1, deadline - time.time())
ready, _, _ = sel.select([fd], [], [], min(remaining, 1.0))
if ready:
chunk = os.read(fd, 65536)
if not chunk:
return None
buf += chunk
if buf.startswith(b"Content-Length:"):
header_end = buf.find(b"\r\n\r\n")
if header_end == -1:
header_end = buf.find(b"\n\n")
delim_len = 2
else:
delim_len = 4
if header_end >= 0:
header = buf[:header_end].decode()
for hline in header.split("\n"):
if hline.strip().lower().startswith("content-length:"):
clen = int(hline.split(":", 1)[1].strip())
body_start = header_end + delim_len
if len(buf) >= body_start + clen:
body = buf[body_start:body_start + clen]
return json.loads(body)
continue
if b"\n" in buf:
line, rest = buf.split(b"\n", 1)
if line.strip():
try:
return json.loads(line)
except json.JSONDecodeError:
buf = rest
continue
return None
def request(self, method, params, req_id, timeout=60):
self.send({"jsonrpc": "2.0", "id": req_id, "method": method, "params": params})
return self.recv(timeout=timeout)
def notify(self, method, params=None):
obj = {"jsonrpc": "2.0", "method": method}
if params:
obj["params"] = params
self.send(obj)
def close(self):
self.proc.terminate()
try:
self.proc.wait(timeout=5)
except subprocess.TimeoutExpired:
self.proc.kill()
return self.proc.stderr.read()
def check(name, response, condition_fn):
global PASS, FAIL
try:
if condition_fn(response):
print(f" \033[32mPASS\033[0m: {name}")
PASS += 1
return True
else:
print(f" \033[31mFAIL\033[0m: {name}")
if response:
print(f" Response: {json.dumps(response, ensure_ascii=False)[:500]}")
else:
print(f" Response: None")
FAIL += 1
return False
except Exception as e:
print(f" \033[31mFAIL\033[0m: {name} — exception: {e}")
FAIL += 1
return False
def get_text(resp):
if not resp or "result" not in resp:
return ""
result = resp["result"]
if isinstance(result, dict):
content = result.get("content", [])
return "".join(c.get("text", "") for c in content if c.get("type") == "text")
return str(result)
def main():
global PASS, FAIL
model_dir = os.path.expanduser("~/.lean-ctx/models")
model_exists = (
os.path.exists(os.path.join(model_dir, "model.onnx")) and
os.path.exists(os.path.join(model_dir, "vocab.txt"))
)
print("\n" + "=" * 60)
print(" Embedding Model + Hybrid Search E2E Test")
print("=" * 60)
if model_exists:
model_size = os.path.getsize(os.path.join(model_dir, "model.onnx"))
vocab_size = os.path.getsize(os.path.join(model_dir, "vocab.txt"))
print(f"\n Model: {model_size / 1024 / 1024:.1f}MB")
print(f" Vocab: {vocab_size / 1024:.0f}KB")
else:
print("\n Model not yet downloaded.")
print(" Starting server to trigger auto-download...")
print(" (This may take 30-60 seconds on first run)")
with tempfile.TemporaryDirectory() as tmpdir:
project_dir = os.path.join(tmpdir, "project")
src_dir = os.path.join(project_dir, "src")
os.makedirs(src_dir)
with open(os.path.join(src_dir, "main.rs"), "w") as f:
f.write("""fn calculate_fibonacci(n: u64) -> u64 {
if n <= 1 { return n; }
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n { let c = a + b; a = b; b = c; }
b
}
fn main() {
println!("fib(10) = {}", calculate_fibonacci(10));
}
""")
with open(os.path.join(src_dir, "auth.rs"), "w") as f:
f.write("""pub struct AuthToken { pub user_id: String, pub permissions: Vec<String> }
pub fn validate_jwt_token(token: &str) -> Result<AuthToken, String> {
if token.is_empty() { return Err("Empty token".into()); }
Ok(AuthToken { user_id: "u1".into(), permissions: vec!["read".into()] })
}
pub fn check_permission(token: &AuthToken, required: &str) -> bool {
token.permissions.iter().any(|p| p == required)
}
""")
with open(os.path.join(src_dir, "utils.rs"), "w") as f:
f.write("""pub fn format_duration(seconds: u64) -> String {
format!("{:02}:{:02}:{:02}", seconds / 3600, (seconds % 3600) / 60, seconds % 60)
}
pub fn parse_csv_line(line: &str) -> Vec<String> {
line.split(',').map(|s| s.trim().to_string()).collect()
}
""")
client = McpClient(BINARY, project_dir)
# Initialize
print("\n--- Initializing server ---")
resp = client.request("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "embedding-test", "version": "1.0.0"}
}, 1)
check("Server initializes", resp, lambda r: r is not None and "result" in r)
client.notify("notifications/initialized")
if not model_exists:
print("\n--- Waiting for model download ---")
wait_start = time.time()
max_wait = 120
while time.time() - wait_start < max_wait:
if os.path.exists(os.path.join(model_dir, "model.onnx")) and \
os.path.exists(os.path.join(model_dir, "vocab.txt")):
elapsed = time.time() - wait_start
print(f" Model downloaded in {elapsed:.1f}s")
break
tmp_file = os.path.join(model_dir, "model.onnx.tmp")
if os.path.exists(tmp_file):
size = os.path.getsize(tmp_file)
print(f" Downloading: {size / 1024 / 1024:.1f}MB...", end="\r")
time.sleep(2)
else:
print(f"\n WARNING: Model download did not complete in {max_wait}s")
model_ready = (
os.path.exists(os.path.join(model_dir, "model.onnx")) and
os.path.exists(os.path.join(model_dir, "vocab.txt"))
)
check("Embedding model available", model_ready, lambda r: r)
if model_ready:
model_size = os.path.getsize(os.path.join(model_dir, "model.onnx"))
vocab_lines = len(open(os.path.join(model_dir, "vocab.txt")).readlines())
print(f" Model: {model_size / 1024 / 1024:.1f}MB, Vocab: {vocab_lines} tokens")
check("Model size > 20MB", model_size, lambda s: s > 20_000_000)
check("Vocab has > 25K tokens", vocab_lines, lambda v: v > 25_000)
# Reindex with embeddings
print("\n--- Reindex with embedding generation ---")
resp = client.request("tools/call", {
"name": "ctx_semantic_search",
"arguments": {"query": "", "path": project_dir, "action": "reindex"}
}, 2, timeout=60)
text = get_text(resp)
print(f" Output: {text}")
check("Reindex completes", resp, lambda r: r is not None)
if model_ready:
check("Embeddings generated during reindex", resp,
lambda r: "embedding" in text.lower())
# Search — should be hybrid mode now
print("\n--- Hybrid search test ---")
resp = client.request("tools/call", {
"name": "ctx_semantic_search",
"arguments": {"query": "fibonacci number calculation", "path": project_dir, "top_k": 5}
}, 3, timeout=30)
text = get_text(resp)
print(f" Output: {text[:300]}")
check("Search returns results", resp, lambda r: r is not None)
if model_ready:
check("Search uses HYBRID mode", resp,
lambda r: "hybrid" in text.lower())
check("Finds fibonacci", resp,
lambda r: "fibonacci" in text.lower() or "main.rs" in text.lower())
# Cross-domain search
print("\n--- Cross-domain semantic search ---")
queries = [
("authentication JWT verify", "auth"),
("parse data comma separated", "csv"),
("time format hours minutes", "duration"),
]
for query, expected in queries:
resp = client.request("tools/call", {
"name": "ctx_semantic_search",
"arguments": {"query": query, "path": project_dir, "top_k": 3}
}, 40 + queries.index((query, expected)), timeout=15)
text = get_text(resp)
check(f"'{query}' → matches '{expected}'", resp,
lambda r, e=expected: e.lower() in get_text(r).lower())
# Final metrics
print("\n--- Embedding telemetry ---")
resp = client.request("tools/call", {
"name": "ctx_metrics",
"arguments": {}
}, 5)
text = get_text(resp)
check("Telemetry shows search data", resp,
lambda r: "search queries" in text.lower() or "Search queries" in text)
if model_ready:
check("Telemetry shows embedding data", resp,
lambda r: "embedding" in text.lower())
stderr = client.close()
print(f"\n{'=' * 60}")
total = PASS + FAIL
if FAIL == 0:
print(f"\033[32m ALL {total} TESTS PASSED\033[0m")
else:
print(f"\033[31m {PASS}/{total} passed, {FAIL} FAILED\033[0m")
if stderr:
print(f"\nServer stderr:")
print(stderr.decode(errors="replace")[-800:])
print(f"{'=' * 60}\n")
sys.exit(1 if FAIL > 0 else 0)
if __name__ == "__main__":
main()