## Summary - **Restructure `src/server/` into subdirectories** (`service/`, `compiler/`, `worker/`, `workspace/`, `protocol/`) to separate concerns: transport/session management, compilation, worker orchestration, and persistent workspace state. - **Decouple MasterServer from transport**: MasterServer no longer holds a `JsonPeer&` reference or registers handlers itself. New `LSPClient` and `AgentClient` classes own their peer references and register protocol handlers, accessing MasterServer internals via `friend class`. - **Add agentic protocol**: A TCP-based side channel (`agentic/compileCommand`) that lets external tools (AI agents, build systems) query compile commands from a running clice server. Includes a CLI client mode (`--mode agentic --port N --path FILE`), server-side listener when `--port` is specified in pipe mode, and integration tests for happy path, fallback, concurrency, and connection-refused. - **Replace fire-and-forget `loop.schedule()` with `kota::task_group`**: Compiler compile tasks, Indexer background indexing + resource monitor, WorkerPool worker monitors, and socket accept loops now use structured concurrency. This eliminates manual `alive_count_`/generation counters and ensures all spawned tasks are joined on shutdown. - **Fix flaky integration test**: `CliceClient.initialize()` now always sets `cache_dir` to a workspace-local `.clice/` directory, preventing stale PCH artifacts from the global `~/.cache/clice/` from polluting test runs. ## Details **Compiler peer lifetime**: `Compiler` and `Indexer` previously took `JsonPeer&` in their constructors, coupling them to a single connection. They now store a `JsonPeer*` set via `set_peer()`, with null checks before sending diagnostics/progress. This supports the multi-connection model where agentic clients don't need diagnostics. **Socket mode single-LSP enforcement**: `accept_connections()` takes a `register_lsp` flag; when true, only the first connection gets an `LSPClient`. All connections get an `AgentClient`. This prevents multiple LSP sessions from racing on shared server state. **Structured shutdown**: `Compiler::stop()` cancels in-flight compile tasks and joins them. `WorkerPool::stop()` signals workers and joins the monitor task group. `Indexer` uses a `cancellation_source` to stop its resource monitor when a background indexing run completes. **Pin kotatsu**: Changed from `GIT_TAG main` + `GIT_SHALLOW TRUE` to an exact commit hash for reproducible builds. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
"""Tests for the agentic CLI client."""
|
|
|
|
import json
|
|
import socket
|
|
import subprocess
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import pytest
|
|
|
|
|
|
def run_agentic(executable, host, port, path, timeout=10):
|
|
result = subprocess.run(
|
|
[
|
|
str(executable),
|
|
"--mode",
|
|
"agentic",
|
|
"--host",
|
|
host,
|
|
"--port",
|
|
str(port),
|
|
"--path",
|
|
path,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
return result
|
|
|
|
|
|
@pytest.mark.workspace("hello_world")
|
|
async def test_compile_command(agentic, workspace):
|
|
executable, host, port = agentic
|
|
main_cpp = (workspace / "main.cpp").as_posix()
|
|
result = run_agentic(executable, host, port, main_cpp)
|
|
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
data = json.loads(result.stdout)
|
|
assert data["file"] == main_cpp
|
|
assert data["directory"] == workspace.as_posix()
|
|
assert len(data["arguments"]) > 0
|
|
|
|
|
|
@pytest.mark.workspace("hello_world")
|
|
async def test_compile_command_fallback(agentic, workspace):
|
|
executable, host, port = agentic
|
|
result = run_agentic(executable, host, port, "/nonexistent/file.cpp")
|
|
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
data = json.loads(result.stdout)
|
|
assert data["file"] == "/nonexistent/file.cpp"
|
|
|
|
|
|
@pytest.mark.workspace("hello_world")
|
|
async def test_multiple_requests(agentic, workspace):
|
|
executable, host, port = agentic
|
|
main_cpp = (workspace / "main.cpp").as_posix()
|
|
for _ in range(3):
|
|
result = run_agentic(executable, host, port, main_cpp)
|
|
assert result.returncode == 0, f"stderr: {result.stderr}"
|
|
data = json.loads(result.stdout)
|
|
assert data["file"] == main_cpp
|
|
|
|
|
|
async def test_connection_refused(executable):
|
|
"""Connecting to a port with no server should fail with non-zero exit."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
free_port = s.getsockname()[1]
|
|
result = run_agentic(executable, "127.0.0.1", free_port, "/some/file.cpp")
|
|
assert result.returncode != 0
|
|
|
|
|
|
@pytest.mark.workspace("hello_world")
|
|
async def test_concurrent_connections(agentic, workspace):
|
|
"""Multiple agentic clients connecting simultaneously should all succeed."""
|
|
executable, host, port = agentic
|
|
main_cpp = (workspace / "main.cpp").as_posix()
|
|
|
|
def do_request(_):
|
|
return run_agentic(executable, host, port, main_cpp)
|
|
|
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
results = list(pool.map(do_request, range(4)))
|
|
|
|
for r in results:
|
|
assert r.returncode == 0, f"stderr: {r.stderr}"
|
|
data = json.loads(r.stdout)
|
|
assert data["file"] == main_cpp
|