Files
clice/tests/integration/agentic/test_cli.py
ykiko 47ad905f5b feat(server): agentic query API, daemon mode, and relay mode (#438)
## 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>
2026-05-04 00:15:37 +08:00

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)