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,156 @@
|
||||
"""Tests for the framework adapters.
|
||||
|
||||
The OpenAI adapter is a pure transformation and is fully tested against a stub
|
||||
server. The framework adapters (LangChain/LlamaIndex/CrewAI) are optional deps;
|
||||
each test runs the adapter if the framework is importable, otherwise asserts the
|
||||
helpful ImportError — so the suite is deterministic with or without them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from leanctx import LeanCtxClient
|
||||
from leanctx.adapters import (
|
||||
coerce_arguments,
|
||||
normalized_tool_specs,
|
||||
run_openai_tool_call,
|
||||
to_crewai_tools,
|
||||
to_langchain_tools,
|
||||
to_llamaindex_tools,
|
||||
to_openai_tools,
|
||||
)
|
||||
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "ctx_read",
|
||||
"description": "Read a file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string"}},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
{"name": "ctx_tree", "description": "List a directory", "input_schema": {"type": "object"}},
|
||||
]
|
||||
|
||||
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args: Any) -> None:
|
||||
pass
|
||||
|
||||
def _send(self, status: int, body: Any) -> None:
|
||||
payload = json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if self.path.startswith("/v1/tools"):
|
||||
self._send(200, {"tools": TOOLS, "total": len(TOOLS), "offset": 0, "limit": 500})
|
||||
else:
|
||||
self._send(404, {"error": "unknown"})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = json.loads(self.rfile.read(length) or b"{}")
|
||||
text = f"called {body.get('name')} with {json.dumps(body.get('arguments', {}), sort_keys=True)}"
|
||||
self._send(200, {"result": {"content": [{"type": "text", "text": text}]}})
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client() -> Tuple[LeanCtxClient, HTTPServer]:
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
host, port = httpd.server_address
|
||||
yield LeanCtxClient(f"http://{host}:{port}"), httpd
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
def test_normalized_specs(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
specs = normalized_tool_specs(c)
|
||||
assert [s.name for s in specs] == ["ctx_read", "ctx_tree"]
|
||||
assert specs[0].parameters["required"] == ["path"]
|
||||
|
||||
|
||||
def test_to_openai_tools_shape(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
tools = to_openai_tools(c)
|
||||
assert tools[0]["type"] == "function"
|
||||
assert tools[0]["function"]["name"] == "ctx_read"
|
||||
assert tools[0]["function"]["parameters"]["properties"]["path"]["type"] == "string"
|
||||
|
||||
|
||||
def test_run_openai_tool_call_dict_and_object(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
# dict shape with JSON-string arguments
|
||||
out = run_openai_tool_call(
|
||||
c, {"function": {"name": "ctx_read", "arguments": '{"path": "README.md"}'}}
|
||||
)
|
||||
assert out == 'called ctx_read with {"path": "README.md"}'
|
||||
|
||||
# object shape with dict arguments
|
||||
class _Fn:
|
||||
name = "ctx_tree"
|
||||
arguments = {"path": "."}
|
||||
|
||||
class _Call:
|
||||
function = _Fn()
|
||||
|
||||
out2 = run_openai_tool_call(c, _Call())
|
||||
assert out2 == 'called ctx_tree with {"path": "."}'
|
||||
|
||||
|
||||
def test_coerce_arguments() -> None:
|
||||
assert coerce_arguments(None) == {}
|
||||
assert coerce_arguments("") == {}
|
||||
assert coerce_arguments('{"a": 1}') == {"a": 1}
|
||||
assert coerce_arguments({"a": 1}) == {"a": 1}
|
||||
assert coerce_arguments("[1,2]") == {} # non-object JSON -> empty
|
||||
|
||||
|
||||
def _has(mod: str) -> bool:
|
||||
# find_spec raises (not returns None) when a parent package is absent.
|
||||
try:
|
||||
return importlib.util.find_spec(mod) is not None
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def test_langchain_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
if _has("langchain_core"):
|
||||
tools = to_langchain_tools(c)
|
||||
assert len(tools) == 2
|
||||
else:
|
||||
with pytest.raises(ImportError, match="langchain-core"):
|
||||
to_langchain_tools(c)
|
||||
|
||||
|
||||
def test_llamaindex_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
if _has("llama_index.core"):
|
||||
tools = to_llamaindex_tools(c)
|
||||
assert len(tools) == 2
|
||||
else:
|
||||
with pytest.raises(ImportError, match="llama-index-core"):
|
||||
to_llamaindex_tools(c)
|
||||
|
||||
|
||||
def test_crewai_adapter(client: Tuple[LeanCtxClient, HTTPServer]) -> None:
|
||||
c, _ = client
|
||||
if _has("crewai"):
|
||||
tools = to_crewai_tools(c)
|
||||
assert len(tools) == 2
|
||||
else:
|
||||
with pytest.raises(ImportError, match="crewai"):
|
||||
to_crewai_tools(c)
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Live adapter smoke tests against a real lean-ctx server (GL #395).
|
||||
|
||||
One end-to-end test per framework adapter (OpenAI / LangChain / LlamaIndex /
|
||||
CrewAI): build the tools from the live ``/v1/tools`` manifest, execute one real
|
||||
tool call through the framework's own invocation path, and assert real output.
|
||||
|
||||
Driven by ``scripts/sdk-conformance.sh`` (CI job ``sdk-conformance``) via
|
||||
``LEANCTX_CONFORMANCE_URL``. Without the URL the suite skips (hermetic local
|
||||
``pytest``); without the optional framework the individual test skips.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from leanctx import LeanCtxClient
|
||||
from leanctx.adapters import (
|
||||
run_openai_tool_call,
|
||||
to_crewai_tools,
|
||||
to_langchain_tools,
|
||||
to_llamaindex_tools,
|
||||
to_openai_tools,
|
||||
)
|
||||
from leanctx.adapters._common import normalized_tool_specs
|
||||
|
||||
URL = os.environ.get("LEANCTX_CONFORMANCE_URL", "").strip()
|
||||
|
||||
pytestmark = pytest.mark.skipif(not URL, reason="LEANCTX_CONFORMANCE_URL not set")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def client() -> LeanCtxClient:
|
||||
return LeanCtxClient(
|
||||
URL, bearer_token=os.environ.get("LEANCTX_CONFORMANCE_TOKEN") or None
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smoke_tool(client: LeanCtxClient) -> str:
|
||||
"""Pick a real tool that needs no arguments, straight from the live manifest."""
|
||||
specs = normalized_tool_specs(client)
|
||||
assert specs, "live server returned no tools"
|
||||
no_arg = [s.name for s in specs if not s.parameters.get("required")]
|
||||
assert no_arg, "no argument-free tool available for the smoke call"
|
||||
# Prefer cheap, read-only diagnostics when present.
|
||||
for preferred in ("ctx_health", "ctx_metrics", "ctx_overview"):
|
||||
if preferred in no_arg:
|
||||
return preferred
|
||||
return no_arg[0]
|
||||
|
||||
|
||||
def _pick(items: list, name_of, name: str) -> object:
|
||||
matches = [t for t in items if name_of(t) == name]
|
||||
assert matches, f"{name} missing from adapter output"
|
||||
return matches[0]
|
||||
|
||||
|
||||
def test_openai_adapter_live_round_trip(client: LeanCtxClient, smoke_tool: str) -> None:
|
||||
specs = to_openai_tools(client)
|
||||
assert specs and all(s["type"] == "function" for s in specs)
|
||||
spec = _pick(specs, lambda s: s["function"]["name"], smoke_tool)
|
||||
|
||||
# The exact dict shape an OpenAI chat completion returns for a tool call.
|
||||
out = run_openai_tool_call(
|
||||
client,
|
||||
{"function": {"name": spec["function"]["name"], "arguments": "{}"}},
|
||||
)
|
||||
assert isinstance(out, str) and out.strip(), "empty tool output"
|
||||
|
||||
|
||||
def test_langchain_adapter_live_round_trip(
|
||||
client: LeanCtxClient, smoke_tool: str
|
||||
) -> None:
|
||||
pytest.importorskip("langchain_core")
|
||||
tool = _pick(to_langchain_tools(client), lambda t: t.name, smoke_tool)
|
||||
out = tool.invoke("{}")
|
||||
assert isinstance(out, str) and out.strip()
|
||||
|
||||
|
||||
def test_llamaindex_adapter_live_round_trip(
|
||||
client: LeanCtxClient, smoke_tool: str
|
||||
) -> None:
|
||||
pytest.importorskip("llama_index.core")
|
||||
tool = _pick(
|
||||
to_llamaindex_tools(client), lambda t: t.metadata.name, smoke_tool
|
||||
)
|
||||
out = tool.call("{}")
|
||||
assert str(out).strip()
|
||||
|
||||
|
||||
def test_crewai_adapter_live_round_trip(
|
||||
client: LeanCtxClient, smoke_tool: str
|
||||
) -> None:
|
||||
pytest.importorskip("crewai")
|
||||
tool = _pick(to_crewai_tools(client), lambda t: t.name, smoke_tool)
|
||||
out = tool.run(arguments="{}")
|
||||
assert str(out).strip()
|
||||
|
||||
|
||||
def test_adapter_specs_cover_full_manifest(client: LeanCtxClient) -> None:
|
||||
"""Drift gate: every tool in the live manifest converts to an OpenAI spec."""
|
||||
manifest_names = {s.name for s in normalized_tool_specs(client)}
|
||||
spec_names = {s["function"]["name"] for s in to_openai_tools(client)}
|
||||
assert spec_names == manifest_names, (
|
||||
f"adapter dropped tools: {sorted(manifest_names - spec_names)}"
|
||||
)
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Integration tests against a real in-process HTTP server (stdlib only)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from leanctx import LeanCtxClient, LeanCtxConfigError, LeanCtxHTTPError
|
||||
|
||||
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args: Any) -> None: # silence test output
|
||||
pass
|
||||
|
||||
def _send(self, status: int, body: Any, content_type: str = "application/json") -> None:
|
||||
payload = body if isinstance(body, bytes) else json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", content_type)
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
if self.path == "/health":
|
||||
self._send(200, b"ok", "text/plain")
|
||||
elif self.path == "/v1/capabilities":
|
||||
self._send(
|
||||
200,
|
||||
{
|
||||
"contract_version": 1,
|
||||
"server": {"name": "lean-ctx", "version": "3.7.5"},
|
||||
"plane": "personal",
|
||||
"transports": ["rest"],
|
||||
"presets": ["coding"],
|
||||
"read_modes": ["full"],
|
||||
"tools": {"total": 1, "names": ["ctx_read"]},
|
||||
"features": {},
|
||||
"extensions": {},
|
||||
"contracts": {},
|
||||
},
|
||||
)
|
||||
elif self.path == "/v1/openapi.json":
|
||||
self._send(200, {"openapi": "3.0.3", "info": {}, "paths": {}})
|
||||
elif self.path.startswith("/v1/tools"):
|
||||
self._send(200, {"tools": [], "total": 0, "offset": 0, "limit": 1})
|
||||
elif self.path == "/v1/notfound":
|
||||
self._send(404, {"error": "nope", "error_code": "E_NOT_FOUND"})
|
||||
else:
|
||||
self._send(404, {"error": "unknown"})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
body = json.loads(raw)
|
||||
# Echo back what we received so the test can assert forwarding.
|
||||
self._send(
|
||||
200,
|
||||
{
|
||||
"result": {
|
||||
"content": [{"type": "text", "text": "tool-ok"}],
|
||||
"echo": body,
|
||||
"workspace": self.headers.get("x-leanctx-workspace"),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def server() -> Tuple[str, HTTPServer]:
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host, port = httpd.server_address
|
||||
yield f"http://{host}:{port}", httpd
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
def test_health(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base)
|
||||
assert client.health() == "ok"
|
||||
|
||||
|
||||
def test_capabilities_and_openapi(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base)
|
||||
caps = client.capabilities()
|
||||
assert caps["contract_version"] == 1
|
||||
assert caps["server"]["version"] == "3.7.5"
|
||||
api = client.openapi()
|
||||
assert api["openapi"].startswith("3.")
|
||||
|
||||
|
||||
def test_list_tools(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base)
|
||||
listing = client.list_tools(limit=1)
|
||||
assert listing["total"] == 0
|
||||
assert listing["tools"] == []
|
||||
|
||||
|
||||
def test_call_tool_forwards_args_and_workspace(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base, workspace_id="ws1", channel_id="ch1")
|
||||
result: Dict[str, Any] = client.call_tool("ctx_read", {"path": "x"})
|
||||
assert result["echo"]["arguments"] == {"path": "x"}
|
||||
assert result["echo"]["workspaceId"] == "ws1"
|
||||
assert result["echo"]["channelId"] == "ch1"
|
||||
assert result["workspace"] == "ws1"
|
||||
|
||||
|
||||
def test_call_tool_text(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base)
|
||||
assert client.call_tool_text("ctx_read", {"path": "x"}) == "tool-ok"
|
||||
|
||||
|
||||
def test_http_error_parsing(server: Tuple[str, HTTPServer]) -> None:
|
||||
base, _ = server
|
||||
client = LeanCtxClient(base)
|
||||
with pytest.raises(LeanCtxHTTPError) as exc:
|
||||
client._get_json("/v1/notfound")
|
||||
assert exc.value.status == 404
|
||||
assert exc.value.error_code == "E_NOT_FOUND"
|
||||
assert exc.value.message == "nope"
|
||||
|
||||
|
||||
def test_invalid_config() -> None:
|
||||
with pytest.raises(LeanCtxConfigError):
|
||||
LeanCtxClient("")
|
||||
client = LeanCtxClient("http://127.0.0.1:9")
|
||||
with pytest.raises(LeanCtxConfigError):
|
||||
client.call_tool("")
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Conformance-kit tests against the in-process stub server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from leanctx import COVERED_ROUTES, LeanCtxClient, run_conformance
|
||||
|
||||
CAPS = {
|
||||
"contract_version": 1,
|
||||
"server": {"name": "lean-ctx", "version": "3.7.5"},
|
||||
"plane": "personal",
|
||||
"transports": ["rest"],
|
||||
"presets": ["coding"],
|
||||
"read_modes": ["full"],
|
||||
"tools": {"total": 1, "names": ["ctx_read"]},
|
||||
"features": {},
|
||||
"extensions": {},
|
||||
"contracts": {"leanctx.contract.http_mcp.contract_version": 1},
|
||||
"contract_status": {"http-mcp": "frozen"},
|
||||
}
|
||||
|
||||
|
||||
def _openapi_paths() -> dict:
|
||||
"""OpenAPI paths mirroring COVERED_ROUTES (the live server's shape)."""
|
||||
paths: dict = {}
|
||||
for route in COVERED_ROUTES:
|
||||
method, path = route.split(" ", 1)
|
||||
paths.setdefault(path, {})[method.lower()] = {"summary": route}
|
||||
return paths
|
||||
|
||||
|
||||
def _make_handler(caps: Any, extra_route: str | None = None):
|
||||
class _Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args: Any) -> None:
|
||||
pass
|
||||
|
||||
def _send(self, status: int, body: Any, ct: str = "application/json") -> None:
|
||||
payload = body if isinstance(body, bytes) else json.dumps(body).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", ct)
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path
|
||||
if path == "/health":
|
||||
self._send(200, b"ok", "text/plain")
|
||||
elif path == "/v1/manifest":
|
||||
self._send(200, {"schema_version": 1, "tools": []})
|
||||
elif path == "/v1/capabilities":
|
||||
self._send(200, caps)
|
||||
elif path == "/v1/openapi.json":
|
||||
paths = _openapi_paths()
|
||||
if extra_route:
|
||||
method, route_path = extra_route.split(" ", 1)
|
||||
paths.setdefault(route_path, {})[method.lower()] = {}
|
||||
self._send(200, {"openapi": "3.0.3", "info": {}, "paths": paths})
|
||||
elif path == "/v1/tools":
|
||||
self._send(200, {"tools": [], "total": 0, "offset": 0, "limit": 1})
|
||||
elif path == "/v1/events":
|
||||
self._send(200, b"", "text/event-stream")
|
||||
elif path == "/v1/context/summary":
|
||||
self._send(
|
||||
200,
|
||||
{
|
||||
"workspaceId": "default",
|
||||
"channelId": "default",
|
||||
"totalEvents": 0,
|
||||
"latestVersion": 0,
|
||||
"activeAgents": [],
|
||||
"recentDecisions": [],
|
||||
"knowledgeDelta": [],
|
||||
"conflictAlerts": [],
|
||||
"eventCountsByKind": {},
|
||||
},
|
||||
)
|
||||
elif path == "/v1/events/search":
|
||||
self._send(200, {"query": "x", "results": [], "count": 0})
|
||||
elif path == "/v1/events/lineage":
|
||||
self._send(200, {"eventId": 1, "chain": [], "depth": 0})
|
||||
elif path == "/v1/metrics":
|
||||
self._send(200, {"events_published": 0})
|
||||
else:
|
||||
self._send(404, {"error": "unknown", "error_code": "not_found"})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
if urlparse(self.path).path == "/v1/tools/call":
|
||||
self._send(
|
||||
404,
|
||||
{"error": "unknown tool", "error_code": "unknown_tool"},
|
||||
)
|
||||
else:
|
||||
self._send(404, {"error": "unknown", "error_code": "not_found"})
|
||||
|
||||
return _Handler
|
||||
|
||||
|
||||
def _serve(caps: Any, extra_route: str | None = None) -> Tuple[str, HTTPServer]:
|
||||
httpd = HTTPServer(("127.0.0.1", 0), _make_handler(caps, extra_route))
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
host, port = httpd.server_address
|
||||
return f"http://{host}:{port}", httpd
|
||||
|
||||
|
||||
def test_conformance_passes_against_valid_server() -> None:
|
||||
base, httpd = _serve(CAPS)
|
||||
try:
|
||||
card = run_conformance(LeanCtxClient(base))
|
||||
assert card.all_passed, [c for c in card.checks if not c.passed]
|
||||
assert card.total == 14
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
def test_conformance_flags_malformed_capabilities() -> None:
|
||||
base, httpd = _serve({"wrong": True})
|
||||
try:
|
||||
card = run_conformance(LeanCtxClient(base))
|
||||
assert not card.all_passed
|
||||
for name in ("capabilities_shape", "contract_status_map", "engine_compat"):
|
||||
failed = next(c for c in card.checks if c.name == name)
|
||||
assert not failed.passed, name
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
|
||||
|
||||
def test_route_coverage_catches_endpoint_drift() -> None:
|
||||
# GL #395 AC 3: a route the SDK does not cover fails within one run.
|
||||
base, httpd = _serve(CAPS, extra_route="GET /v1/brand-new-route")
|
||||
try:
|
||||
card = run_conformance(LeanCtxClient(base))
|
||||
coverage = next(c for c in card.checks if c.name == "route_coverage")
|
||||
assert not coverage.passed
|
||||
assert "GET /v1/brand-new-route" in coverage.detail
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Live conformance run against a real lean-ctx server (GL #395).
|
||||
|
||||
Driven by ``scripts/sdk-conformance.sh`` (CI job ``sdk-conformance``): the
|
||||
script builds the engine, starts ``lean-ctx serve`` and exports
|
||||
``LEANCTX_CONFORMANCE_URL``. Without that variable the suite skips, so plain
|
||||
``pytest`` runs stay hermetic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from leanctx import LeanCtxClient, run_conformance
|
||||
|
||||
URL = os.environ.get("LEANCTX_CONFORMANCE_URL", "").strip()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not URL, reason="LEANCTX_CONFORMANCE_URL not set")
|
||||
def test_live_conformance_all_checks_pass() -> None:
|
||||
client = LeanCtxClient(
|
||||
URL, bearer_token=os.environ.get("LEANCTX_CONFORMANCE_TOKEN") or None
|
||||
)
|
||||
card = run_conformance(client)
|
||||
|
||||
matrix_dir = os.environ.get("LEANCTX_MATRIX_DIR", "").strip()
|
||||
if matrix_dir:
|
||||
out = pathlib.Path(matrix_dir) / "conformance-python.json"
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"sdk": "python",
|
||||
"passed": card.passed,
|
||||
"total": card.total,
|
||||
"all_passed": card.all_passed,
|
||||
"checks": [
|
||||
{"name": c.name, "passed": c.passed, "detail": c.detail}
|
||||
for c in card.checks
|
||||
],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
failed = [f"{c.name}: {c.detail}" for c in card.checks if not c.passed]
|
||||
assert card.all_passed, failed
|
||||
Reference in New Issue
Block a user