## Summary - **Agentic query API**: 11 JSON-RPC handlers over TCP for AI agent integration — compileCommand, projectFiles, fileDeps, impactAnalysis, symbolSearch, readSymbol, documentSymbols, definition, references, callGraph, typeHierarchy - **Daemon mode**: MasterServer listens on a unix domain socket, accepting multiple agent connections (`--mode daemon`) - **Relay mode**: Bidirectional stdin/stdout ↔ unix socket proxy for editor integration (`--mode relay`) - **Full-body definition text**: readSymbol/definition return complete function/class bodies via brace matching, not just the declaration line - **24 integration tests** with concrete value assertions covering all handlers ## Test plan - [x] All 551 unit tests pass - [x] All 2 smoke tests pass - [x] All 148 integration tests pass (including 24 new agentic tests) - [x] Raw JSON responses manually inspected for correctness (line numbers, text content, field names, structural completeness) - [x] Formatted with `pixi run format` 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Daemon mode: background service with workspace watching and socket support * Relay mode: stdio ↔ Unix-socket message relay * Expanded agentic RPC/CLI: project files, deps/impact, symbol search/read, document symbols, definitions, references, call graphs, type hierarchy, status, shutdown; richer query options * **Behavioral** * Improved indexing lifecycle and status reporting; safer shutdown coordination and client handling * **Tests** * New integration tests covering agentic RPC endpoints, CLI status, shutdown, and error cases <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
190 lines
6.4 KiB
Python
190 lines
6.4 KiB
Python
"""CLI-based tests for agentic mode — run clice --mode agentic as a subprocess."""
|
|
|
|
import json
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from tests.integration.utils.wait import wait_for_index
|
|
|
|
|
|
def run_cli(executable, host, port, method, **kwargs):
|
|
cmd = [
|
|
str(executable),
|
|
"--mode",
|
|
"agentic",
|
|
"--host",
|
|
host,
|
|
"--port",
|
|
str(port),
|
|
"--method",
|
|
method,
|
|
]
|
|
for k, v in kwargs.items():
|
|
cmd.extend([f"--{k}", str(v)])
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
return result
|
|
|
|
|
|
@pytest.fixture
|
|
async def indexed_server(request, executable, workspace):
|
|
"""Start server with LSP+agentic, compile a file, wait for indexing."""
|
|
import asyncio
|
|
from tests.integration.utils.client import CliceClient
|
|
from tests.conftest import _shutdown_client, _find_free_port
|
|
|
|
host = "127.0.0.1"
|
|
port = _find_free_port()
|
|
cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)]
|
|
|
|
c = CliceClient()
|
|
await c.start_io(*cmd)
|
|
|
|
init_options = {"project": {"cache_dir": str(workspace / ".clice")}}
|
|
await c.initialize(workspace, initialization_options=init_options)
|
|
|
|
uri, _ = await c.open_and_wait(workspace / "main.cpp")
|
|
assert await wait_for_index(c, uri, "add"), "Index not ready"
|
|
|
|
from tests.integration.agentic.test_agentic import AgenticRpcClient
|
|
|
|
rpc = AgenticRpcClient(host, port)
|
|
for _ in range(30):
|
|
resp = rpc.request("agentic/symbolSearch", {"query": "add"})
|
|
if "result" in resp and resp["result"]["symbols"]:
|
|
break
|
|
await asyncio.sleep(1)
|
|
rpc.close()
|
|
|
|
yield executable, host, port, workspace
|
|
|
|
c.close(uri)
|
|
await _shutdown_client(c)
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_compile_command(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
path = (workspace / "main.cpp").as_posix()
|
|
r = run_cli(exe, host, port, "compileCommand", path=path)
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["file"] == path
|
|
assert len(data["arguments"]) > 0
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_symbol_search(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "symbolSearch", query="add")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
names = [s["name"] for s in data["symbols"]]
|
|
assert "add" in names
|
|
add_sym = next(s for s in data["symbols"] if s["name"] == "add")
|
|
assert add_sym["kind"] == "Function"
|
|
assert add_sym["line"] == 19
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_definition(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "definition", name="add")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["name"] == "add"
|
|
defn = data["definition"]
|
|
assert defn["startLine"] == 19
|
|
assert defn["endLine"] == 21
|
|
assert "return a + b;" in defn["text"]
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_definition_by_position(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
path = (workspace / "main.cpp").as_posix()
|
|
r = run_cli(exe, host, port, "definition", path=path, line=19)
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["name"] == "add"
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_references(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "references", name="global_var")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["name"] == "global_var"
|
|
assert data["total"] == 2
|
|
lines = sorted(ref["line"] for ref in data["references"])
|
|
assert lines == [34, 38]
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_read_symbol(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "readSymbol", name="compute")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["name"] == "compute"
|
|
assert "add(1, 2)" in data["text"]
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_document_symbols(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
path = (workspace / "main.cpp").as_posix()
|
|
r = run_cli(exe, host, port, "documentSymbols", path=path)
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
names = [s["name"] for s in data["symbols"]]
|
|
assert "add" in names
|
|
assert "main" in names
|
|
assert "global_var" in names
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_call_graph(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "callGraph", name="add", direction="callers")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["root"]["name"] == "add"
|
|
caller_names = [c["name"] for c in data["callers"]]
|
|
assert "compute" in caller_names
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_type_hierarchy(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "typeHierarchy", name="Dog", direction="supertypes")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["root"]["name"] == "Dog"
|
|
supertype_names = [t["name"] for t in data["supertypes"]]
|
|
assert "Animal" in supertype_names
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_project_files(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "projectFiles")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["total"] > 0
|
|
paths = [f["path"] for f in data["files"]]
|
|
assert any("main.cpp" in p for p in paths)
|
|
|
|
|
|
@pytest.mark.workspace("index_features")
|
|
async def test_cli_status(indexed_server, workspace):
|
|
exe, host, port, _ = indexed_server
|
|
r = run_cli(exe, host, port, "status")
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert isinstance(data["idle"], bool)
|
|
assert data["total"] > 0
|
|
assert isinstance(data["pending"], int)
|
|
assert isinstance(data["indexed"], int)
|