Files
cleveragents-core/features/steps/lsp_lifecycle_coverage_steps.py
T
freemo 31472b5413
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 21s
CI / helm (pull_request) Successful in 22s
CI / lint (pull_request) Successful in 3m20s
CI / quality (pull_request) Successful in 3m43s
CI / typecheck (pull_request) Successful in 3m58s
CI / security (pull_request) Successful in 4m8s
CI / integration_tests (pull_request) Successful in 9m33s
CI / unit_tests (pull_request) Successful in 10m12s
CI / docker (pull_request) Successful in 1m28s
CI / coverage (pull_request) Successful in 12m18s
CI / e2e_tests (pull_request) Successful in 19m51s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 15s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 3m18s
CI / quality (push) Successful in 3m41s
CI / typecheck (push) Successful in 3m57s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m10s
CI / unit_tests (push) Failing after 6m58s
CI / docker (push) Has been skipped
CI / integration_tests (push) Successful in 9m15s
CI / coverage (push) Successful in 12m26s
CI / e2e_tests (push) Successful in 23m11s
CI / status-check (push) Failing after 1s
CI / benchmark-publish (push) Successful in 28m31s
CI / benchmark-regression (pull_request) Successful in 54m53s
test(coverage): add Behave scenarios for 39 under-covered modules
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate.

ISSUES CLOSED: #1232
2026-03-31 21:47:12 +00:00

636 lines
22 KiB
Python

"""Step definitions for lsp_lifecycle_coverage.feature.
Exercises every uncovered code path in cleveragents.lsp.lifecycle including
_ManagedServer construction, start/stop/restart lifecycle, race conditions,
health checks, error handling, stop_all, and list_running.
All external I/O (StdioTransport, LspClient) is mocked so no real
subprocesses are spawned.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.lsp.errors import LspServerNotFoundError
from cleveragents.lsp.lifecycle import LspLifecycleManager, _ManagedServer
from cleveragents.lsp.models import LspServerConfig
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_config(name: str, command: str) -> LspServerConfig:
"""Create a minimal LspServerConfig for testing."""
return LspServerConfig(name=name, command=command)
def _make_mock_transport(alive: bool = True) -> MagicMock:
"""Return a mock StdioTransport that claims to be alive."""
t = MagicMock()
t.is_alive = alive
t.start = MagicMock()
t.stop = MagicMock()
return t
def _make_mock_client(initialized: bool = True) -> MagicMock:
"""Return a mock LspClient."""
c = MagicMock()
c.is_initialized = initialized
c.initialize = MagicMock()
c.shutdown = MagicMock()
return c
def _patch_transport_and_client(context: Context, transport=None, client=None):
"""Patch StdioTransport and LspClient constructors.
Returns (mock_transport_instance, mock_client_instance).
"""
mock_transport = transport or _make_mock_transport()
mock_client = client or _make_mock_client()
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=mock_transport,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=mock_client,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
return mock_transport, mock_client
class _RaceInjectingDict(dict):
"""A dict subclass that injects a race winner on the second .get() call
for a particular key, simulating another thread registering a server
between Phase 1 and Phase 3 of start_server.
"""
def __init__(self, *args, target_key=None, race_winner=None, **kwargs):
super().__init__(*args, **kwargs)
self._target_key = target_key
self._race_winner = race_winner
self._get_count = 0
def get(self, key, default=None):
result = super().get(key, default)
if key == self._target_key:
self._get_count += 1
if self._get_count == 2:
# Simulate another thread having registered this key
self[key] = self._race_winner
return self._race_winner
return result
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("llcov a fresh LspLifecycleManager instance")
def step_fresh_manager(context: Context) -> None:
context.llcov_manager = LspLifecycleManager()
context.llcov_config: LspServerConfig | None = None
context.llcov_config2: LspServerConfig | None = None
context.llcov_client = None
context.llcov_original_client = None
context.llcov_error = None
context.llcov_result = None
context.llcov_health = None
context.llcov_running_list = None
context.llcov_mock_transport = None
context.llcov_mock_client = None
context.llcov_old_transport = None
context.llcov_race_transport = None
context.llcov_race_client = None
context.llcov_new_transport_stopped = False
# ---------------------------------------------------------------------------
# Config creation
# ---------------------------------------------------------------------------
@given('llcov a server config named "{name}" with command "{command}"')
def step_create_config(context: Context, name: str, command: str) -> None:
cfg = _make_config(name, command)
# If first config already set, store as config2
if context.llcov_config is not None:
context.llcov_config2 = cfg
else:
context.llcov_config = cfg
# ---------------------------------------------------------------------------
# Start server (happy path)
# ---------------------------------------------------------------------------
@given('llcov the server is already started for workspace "{workspace}"')
def step_server_already_started(context: Context, workspace: str) -> None:
mock_transport, mock_client = _patch_transport_and_client(context)
context.llcov_mock_transport = mock_transport
context.llcov_mock_client = mock_client
context.llcov_original_client = context.llcov_manager.start_server(
context.llcov_config, workspace
)
@given('llcov the second server is already started for workspace "{workspace}"')
def step_second_server_already_started(context: Context, workspace: str) -> None:
mock_transport2 = _make_mock_transport()
mock_client2 = _make_mock_client()
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=mock_transport2,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=mock_client2,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
context.llcov_manager.start_server(context.llcov_config2, workspace)
@when('llcov I start the server for workspace "{workspace}"')
def step_start_server(context: Context, workspace: str) -> None:
mock_transport, mock_client = _patch_transport_and_client(context)
context.llcov_mock_transport = mock_transport
context.llcov_mock_client = mock_client
context.llcov_client = context.llcov_manager.start_server(
context.llcov_config, workspace
)
# ---------------------------------------------------------------------------
# Start server: reuse existing alive server
# ---------------------------------------------------------------------------
@given("llcov the server is started again to bump ref count")
def step_bump_ref_count(context: Context) -> None:
"""Start the server a second time to bump ref_count to 2."""
context.llcov_manager.start_server(context.llcov_config, "/tmp/ws")
@when('llcov I start the server again for workspace "{workspace}"')
def step_start_server_again(context: Context, workspace: str) -> None:
# The existing transport is still alive, so Phase 1 should return existing client
context.llcov_client = context.llcov_manager.start_server(
context.llcov_config, workspace
)
# ---------------------------------------------------------------------------
# Start server: initialize failure
# ---------------------------------------------------------------------------
@given("llcov the LspClient initialize will raise an error")
def step_client_init_will_fail(context: Context) -> None:
context.llcov_init_fails = True
@when('llcov I attempt to start the server for workspace "{workspace}"')
def step_attempt_start_server(context: Context, workspace: str) -> None:
mock_transport = _make_mock_transport()
mock_client = _make_mock_client()
mock_client.initialize.side_effect = RuntimeError("init boom")
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=mock_transport,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=mock_client,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
context.llcov_mock_transport = mock_transport
context.llcov_mock_client = mock_client
try:
context.llcov_client = context.llcov_manager.start_server(
context.llcov_config, workspace
)
except Exception as exc:
context.llcov_error = exc
# ---------------------------------------------------------------------------
# Start server: race condition
# ---------------------------------------------------------------------------
@given('llcov another thread already registered "{name}" before Phase 3')
def step_setup_race_condition(context: Context, name: str) -> None:
"""Pre-build a race winner that will be injected into _servers."""
context.llcov_race_transport = _make_mock_transport(alive=True)
context.llcov_race_client = _make_mock_client()
@when('llcov I start the server for workspace "{workspace}" with race')
def step_start_server_with_race(context: Context, workspace: str) -> None:
"""Simulate start_server where another thread wins the race in Phase 3.
We replace ``manager._servers`` with a ``_RaceInjectingDict`` that
injects the race winner on the second ``.get()`` call (Phase 3),
while the first ``.get()`` (Phase 1) returns None normally.
"""
phase2_transport = _make_mock_transport(alive=True)
phase2_client = _make_mock_client()
race_winner = _ManagedServer(
config=context.llcov_config,
transport=context.llcov_race_transport,
client=context.llcov_race_client,
workspace_path=workspace,
)
# Replace _servers with our race-injecting dict
race_dict = _RaceInjectingDict(
target_key=context.llcov_config.name,
race_winner=race_winner,
)
context.llcov_manager._servers = race_dict
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=phase2_transport,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=phase2_client,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
context.llcov_client = context.llcov_manager.start_server(
context.llcov_config, workspace
)
context.llcov_phase2_transport = phase2_transport
# ---------------------------------------------------------------------------
# Stop server
# ---------------------------------------------------------------------------
@when('llcov I stop the server "{name}"')
def step_stop_server(context: Context, name: str) -> None:
context.llcov_manager.stop_server(name)
@when('llcov I attempt to stop server "{name}"')
def step_attempt_stop_server(context: Context, name: str) -> None:
try:
context.llcov_manager.stop_server(name)
except Exception as exc:
context.llcov_error = exc
# ---------------------------------------------------------------------------
# Shutdown (direct call)
# ---------------------------------------------------------------------------
@given("llcov the client shutdown will raise an exception")
def step_client_shutdown_will_fail(context: Context) -> None:
context.llcov_mock_client.shutdown.side_effect = RuntimeError("shutdown boom")
@given("llcov the transport stop will raise an exception")
def step_transport_stop_will_fail(context: Context) -> None:
context.llcov_mock_transport.stop.side_effect = RuntimeError("transport stop boom")
@when("llcov I trigger shutdown on the managed server")
def step_trigger_shutdown(context: Context) -> None:
name = context.llcov_config.name
managed = context.llcov_manager._servers[name]
context.llcov_manager._shutdown_server(managed)
# ---------------------------------------------------------------------------
# Get client
# ---------------------------------------------------------------------------
@when('llcov I get the client for "{name}"')
def step_get_client(context: Context, name: str) -> None:
context.llcov_client = context.llcov_manager.get_client(name)
@when('llcov I attempt to get the client for "{name}"')
def step_attempt_get_client(context: Context, name: str) -> None:
try:
context.llcov_client = context.llcov_manager.get_client(name)
except Exception as exc:
context.llcov_error = exc
# ---------------------------------------------------------------------------
# Health check
# ---------------------------------------------------------------------------
@when('llcov I health check "{name}"')
def step_health_check(context: Context, name: str) -> None:
context.llcov_health = context.llcov_manager.health_check(name)
# ---------------------------------------------------------------------------
# Restart server
# ---------------------------------------------------------------------------
@when('llcov I restart the server "{name}"')
def step_restart_server(context: Context, name: str) -> None:
context.llcov_old_transport = context.llcov_mock_transport
new_transport = _make_mock_transport()
new_client = _make_mock_client()
context.llcov_new_transport = new_transport
context.llcov_new_client = new_client
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=new_transport,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=new_client,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
context.llcov_client = context.llcov_manager.restart_server(name)
@when('llcov I attempt to restart server "{name}"')
def step_attempt_restart_server(context: Context, name: str) -> None:
try:
# For "not found" case, don't set up mocks
if not context.llcov_manager._servers.get(name):
context.llcov_manager.restart_server(name)
else:
# For init failure during restart
context.llcov_old_transport = context.llcov_mock_transport
new_transport = _make_mock_transport()
new_client = _make_mock_client()
new_client.initialize.side_effect = RuntimeError("restart init boom")
context.llcov_new_restart_transport = new_transport
p_transport = patch(
"cleveragents.lsp.lifecycle.StdioTransport",
return_value=new_transport,
)
p_client = patch(
"cleveragents.lsp.lifecycle.LspClient",
return_value=new_client,
)
p_transport.start()
p_client.start()
context.add_cleanup(p_transport.stop)
context.add_cleanup(p_client.stop)
context.llcov_client = context.llcov_manager.restart_server(name)
except Exception as exc:
context.llcov_error = exc
@given("llcov the LspClient initialize will raise on restart")
def step_init_fails_on_restart(context: Context) -> None:
context.llcov_init_fails_on_restart = True
# ---------------------------------------------------------------------------
# Stop all
# ---------------------------------------------------------------------------
@when("llcov I stop all servers")
def step_stop_all(context: Context) -> None:
context.llcov_manager.stop_all()
# ---------------------------------------------------------------------------
# List running
# ---------------------------------------------------------------------------
@when("llcov I list running servers")
def step_list_running(context: Context) -> None:
context.llcov_running_list = context.llcov_manager.list_running()
# ---------------------------------------------------------------------------
# Then assertions
# ---------------------------------------------------------------------------
@then("llcov the returned client should not be None")
def step_client_not_none(context: Context) -> None:
assert context.llcov_client is not None, "Expected a client but got None"
@then('llcov the server "{name}" should be registered')
def step_server_registered(context: Context, name: str) -> None:
assert name in context.llcov_manager._servers, (
f"Server '{name}' not found in _servers"
)
@then('llcov the server "{name}" should not be registered')
def step_server_not_registered(context: Context, name: str) -> None:
assert name not in context.llcov_manager._servers, (
f"Server '{name}' should not be in _servers"
)
@then('llcov the server "{name}" should still be registered')
def step_server_still_registered(context: Context, name: str) -> None:
assert name in context.llcov_manager._servers, (
f"Server '{name}' should still be in _servers"
)
@then("llcov the returned client should be the same as the original")
def step_client_same_as_original(context: Context) -> None:
assert context.llcov_client is context.llcov_original_client, (
"Expected reused client to be the same object"
)
@then('llcov the ref count for "{name}" should be {count:d}')
def step_ref_count(context: Context, name: str, count: int) -> None:
managed = context.llcov_manager._servers[name]
assert managed.ref_count == count, (
f"Expected ref_count={count}, got {managed.ref_count}"
)
@then("llcov a start error should have been raised")
def step_start_error_raised(context: Context) -> None:
assert context.llcov_error is not None, "Expected an error but none was raised"
assert isinstance(context.llcov_error, RuntimeError), (
f"Expected RuntimeError, got {type(context.llcov_error)}"
)
@then("llcov the transport should have been stopped after init failure")
def step_transport_stopped_after_init(context: Context) -> None:
context.llcov_mock_transport.stop.assert_called()
@then("llcov the returned client should be the race winner client")
def step_client_is_race_winner(context: Context) -> None:
assert context.llcov_client is context.llcov_race_client, (
"Expected the race winner client"
)
@then("llcov the losing transport should have been stopped")
def step_losing_transport_stopped(context: Context) -> None:
context.llcov_phase2_transport.stop.assert_called()
@then("llcov shutdown should have been called on the client")
def step_shutdown_called(context: Context) -> None:
context.llcov_mock_client.shutdown.assert_called()
@then("llcov a LspServerNotFoundError should have been raised")
def step_not_found_error(context: Context) -> None:
assert context.llcov_error is not None, "Expected an error but none was raised"
assert isinstance(context.llcov_error, LspServerNotFoundError), (
f"Expected LspServerNotFoundError, got {type(context.llcov_error)}"
)
@then("llcov the client shutdown method should have been called")
def step_client_shutdown_called(context: Context) -> None:
context.llcov_mock_client.shutdown.assert_called()
@then("llcov the transport stop method should have been called")
def step_transport_stop_called(context: Context) -> None:
context.llcov_mock_transport.stop.assert_called()
@then("llcov the transport stop method should have been called despite error")
def step_transport_stop_called_despite_error(context: Context) -> None:
context.llcov_mock_transport.stop.assert_called()
@then("llcov the health check result should be true")
def step_health_true(context: Context) -> None:
assert context.llcov_health is True, f"Expected True, got {context.llcov_health}"
@then("llcov the health check result should be false")
def step_health_false(context: Context) -> None:
assert context.llcov_health is False, f"Expected False, got {context.llcov_health}"
@then("llcov the old transport should have been stopped")
def step_old_transport_stopped(context: Context) -> None:
context.llcov_old_transport.stop.assert_called()
@then("llcov the managed server should have the new transport and client")
def step_managed_has_new(context: Context) -> None:
name = context.llcov_config.name
managed = context.llcov_manager._servers[name]
assert managed.transport is context.llcov_new_transport, (
"Managed server should have the new transport"
)
assert managed.client is context.llcov_new_client, (
"Managed server should have the new client"
)
@then("llcov a restart error should have been raised")
def step_restart_error_raised(context: Context) -> None:
assert context.llcov_error is not None, "Expected an error but none was raised"
assert isinstance(context.llcov_error, RuntimeError), (
f"Expected RuntimeError, got {type(context.llcov_error)}"
)
@then("llcov the new transport should have been stopped after restart failure")
def step_new_transport_stopped_after_restart(context: Context) -> None:
context.llcov_new_restart_transport.stop.assert_called()
@then("llcov no servers should be registered")
def step_no_servers(context: Context) -> None:
assert len(context.llcov_manager._servers) == 0, (
f"Expected 0 servers, got {len(context.llcov_manager._servers)}"
)
@then("llcov the running list should contain {count:d} entry")
def step_running_list_count_singular(context: Context, count: int) -> None:
assert len(context.llcov_running_list) == count, (
f"Expected {count} entries, got {len(context.llcov_running_list)}"
)
@then("llcov the running list should contain {count:d} entries")
def step_running_list_count_plural(context: Context, count: int) -> None:
assert len(context.llcov_running_list) == count, (
f"Expected {count} entries, got {len(context.llcov_running_list)}"
)
@then('llcov the running entry for "{name}" should have workspace "{workspace}"')
def step_running_entry_workspace(context: Context, name: str, workspace: str) -> None:
entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
assert entry is not None, f"No entry found for '{name}'"
assert entry["workspace"] == workspace, (
f"Expected workspace='{workspace}', got '{entry['workspace']}'"
)
@then('llcov the running entry for "{name}" should have alive status true')
def step_running_entry_alive(context: Context, name: str) -> None:
entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
assert entry is not None, f"No entry found for '{name}'"
assert entry["alive"] is True, f"Expected alive=True, got {entry['alive']}"
@then('llcov the running entry for "{name}" should have ref count {count:d}')
def step_running_entry_ref_count(context: Context, name: str, count: int) -> None:
entry = next((e for e in context.llcov_running_list if e["name"] == name), None)
assert entry is not None, f"No entry found for '{name}'"
assert entry["ref_count"] == count, (
f"Expected ref_count={count}, got {entry['ref_count']}"
)