#!/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 } pub fn validate_jwt_token(token: &str) -> Result { 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 { 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()