forked from HAL9000/cleveragents-core
be69a49eff
Add the missing `-u` short-form alias for the `--update` option on the
`lsp add` command, aligning the implementation with the specification
(spec line 8645: `agents lsp add [--update|-u] (--config|-c) <FILE>`).
The Typer option declaration in `lsp.py` now includes `"-u"` alongside
`"--update"`. A new Behave scenario ("Add LSP server with -u short form
overwrites existing") and its step definition verify the short form works
end-to-end through the CLI runner.
No conflicts exist — `-u` was not claimed by any other option on
`lsp add` (existing short forms: `-c` for `--config`, `-f` for
`--format`).
ISSUES CLOSED: #912
Co-authored-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-committed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
373 lines
12 KiB
Python
373 lines
12 KiB
Python
"""Step definitions for LSP CLI commands coverage tests.
|
|
|
|
Covers all four CLI commands in cleveragents.cli.commands.lsp:
|
|
- add (register from YAML config)
|
|
- remove (unregister a server)
|
|
- list (list registered servers)
|
|
- show (show server details)
|
|
|
|
Also covers the module-level helpers _reset_registry and _get_registry.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands.lsp import _get_registry, _reset_registry, app
|
|
from cleveragents.lsp.registry import LspRegistry
|
|
|
|
_runner = CliRunner()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# YAML fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_VALID_YAML = """\
|
|
name: local/test-lsp
|
|
command: test-langserver
|
|
args:
|
|
- "--stdio"
|
|
languages:
|
|
- python
|
|
capabilities:
|
|
- diagnostics
|
|
- completions
|
|
- hover
|
|
"""
|
|
|
|
_VALID_YAML_2 = """\
|
|
name: devops/clangd
|
|
command: clangd
|
|
args:
|
|
- "--background-index"
|
|
languages:
|
|
- c
|
|
- cpp
|
|
capabilities:
|
|
- diagnostics
|
|
- document_symbols
|
|
"""
|
|
|
|
_VALID_YAML_ENV = """\
|
|
name: local/test-lsp-env
|
|
command: test-langserver-env
|
|
args:
|
|
- "--stdio"
|
|
languages:
|
|
- python
|
|
capabilities:
|
|
- diagnostics
|
|
env:
|
|
VIRTUAL_ENV: /tmp/venv
|
|
PATH: /tmp/venv/bin
|
|
"""
|
|
|
|
_LIST_YAML = """\
|
|
- name: invalid
|
|
command: invalid
|
|
"""
|
|
|
|
_BAD_SCHEMA_YAML = """\
|
|
name: missing-slash-in-name
|
|
command: some-command
|
|
languages:
|
|
- python
|
|
capabilities:
|
|
- diagnostics
|
|
"""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper to write temp YAML
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _write_temp_yaml(content: str) -> str:
|
|
"""Write content to a temporary YAML file and return its path."""
|
|
fd, path = tempfile.mkstemp(suffix=".yaml")
|
|
with os.fdopen(fd, "w") as fh:
|
|
fh.write(content)
|
|
return path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a clean LSP CLI test environment")
|
|
def step_clean_lsp_cli_env(context: Context) -> None:
|
|
_reset_registry()
|
|
context.lsp_cli_result = None
|
|
context.lsp_yaml_path = None
|
|
context.lsp_invalid_yaml_path = None
|
|
context.lsp_bad_schema_yaml_path = None
|
|
context.lsp_env_yaml_path = None
|
|
context._cleanup_paths: list[str] = []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a valid LSP server YAML config file")
|
|
def step_valid_lsp_yaml(context: Context) -> None:
|
|
path = _write_temp_yaml(_VALID_YAML)
|
|
context.lsp_yaml_path = path
|
|
context._cleanup_paths.append(path)
|
|
|
|
|
|
@given("a YAML config file containing a list instead of a mapping")
|
|
def step_invalid_yaml_list(context: Context) -> None:
|
|
path = _write_temp_yaml(_LIST_YAML)
|
|
context.lsp_invalid_yaml_path = path
|
|
context._cleanup_paths.append(path)
|
|
|
|
|
|
@given("a YAML config file with invalid schema fields")
|
|
def step_bad_schema_yaml(context: Context) -> None:
|
|
path = _write_temp_yaml(_BAD_SCHEMA_YAML)
|
|
context.lsp_bad_schema_yaml_path = path
|
|
context._cleanup_paths.append(path)
|
|
|
|
|
|
@given("a valid LSP server YAML config file with env vars")
|
|
def step_valid_lsp_yaml_env(context: Context) -> None:
|
|
path = _write_temp_yaml(_VALID_YAML_ENV)
|
|
context.lsp_env_yaml_path = path
|
|
context._cleanup_paths.append(path)
|
|
|
|
|
|
@given("the LSP server has been registered via CLI")
|
|
def step_lsp_server_registered(context: Context) -> None:
|
|
result = _runner.invoke(app, ["add", "--config", context.lsp_yaml_path])
|
|
assert result.exit_code == 0, (
|
|
f"Pre-registration failed (exit {result.exit_code}): {result.output}"
|
|
)
|
|
|
|
|
|
@given("the LSP server with env has been registered via CLI")
|
|
def step_lsp_server_env_registered(context: Context) -> None:
|
|
result = _runner.invoke(app, ["add", "--config", context.lsp_env_yaml_path])
|
|
assert result.exit_code == 0, (
|
|
f"Pre-registration failed (exit {result.exit_code}): {result.output}"
|
|
)
|
|
|
|
|
|
@given("multiple LSP servers have been registered via CLI")
|
|
def step_multiple_lsp_servers_registered(context: Context) -> None:
|
|
path1 = _write_temp_yaml(_VALID_YAML)
|
|
path2 = _write_temp_yaml(_VALID_YAML_2)
|
|
context._cleanup_paths.extend([path1, path2])
|
|
|
|
result1 = _runner.invoke(app, ["add", "--config", path1])
|
|
assert result1.exit_code == 0, (
|
|
f"Registration 1 failed (exit {result1.exit_code}): {result1.output}"
|
|
)
|
|
|
|
result2 = _runner.invoke(app, ["add", "--config", path2])
|
|
assert result2.exit_code == 0, (
|
|
f"Registration 2 failed (exit {result2.exit_code}): {result2.output}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps — lsp add
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run lsp CLI add with --config pointing to the YAML file")
|
|
def step_run_lsp_add(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_yaml_path]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config pointing to a non-existent file")
|
|
def step_run_lsp_add_missing(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", "/tmp/nonexistent_lsp_config_abcdef.yaml"]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config pointing to the invalid YAML file")
|
|
def step_run_lsp_add_invalid_yaml(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_invalid_yaml_path]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config pointing to the bad schema YAML file")
|
|
def step_run_lsp_add_bad_schema(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_bad_schema_yaml_path]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config and --update")
|
|
def step_run_lsp_add_update(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_yaml_path, "--update"]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config and -u short form")
|
|
def step_run_lsp_add_u_short(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_yaml_path, "-u"]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config and --format json")
|
|
def step_run_lsp_add_json(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_yaml_path, "--format", "json"]
|
|
)
|
|
|
|
|
|
@when("I run lsp CLI add with --config without --update")
|
|
def step_run_lsp_add_no_update(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["add", "--config", context.lsp_yaml_path]
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps — lsp remove
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I run lsp CLI remove with name "{name}" and --yes')
|
|
def step_run_lsp_remove(context: Context, name: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["remove", name, "--yes"])
|
|
|
|
|
|
@when('I run lsp CLI remove with name "{name}" and --yes and --format json')
|
|
def step_run_lsp_remove_json(context: Context, name: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(
|
|
app, ["remove", name, "--yes", "--format", "json"]
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps — lsp list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I run lsp CLI list")
|
|
def step_run_lsp_list(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["list"])
|
|
|
|
|
|
@when('I run lsp CLI list with --namespace "{ns}"')
|
|
def step_run_lsp_list_namespace(context: Context, ns: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["list", "--namespace", ns])
|
|
|
|
|
|
@when("I run lsp CLI list with --format json")
|
|
def step_run_lsp_list_json(context: Context) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["list", "--format", "json"])
|
|
|
|
|
|
@when('I run lsp CLI list with --language "{lang}"')
|
|
def step_run_lsp_list_language(context: Context, lang: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["list", "--language", lang])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps — lsp show
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I run lsp CLI show with name "{name}"')
|
|
def step_run_lsp_show(context: Context, name: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["show", name])
|
|
|
|
|
|
@when('I run lsp CLI show with name "{name}" and --format json')
|
|
def step_run_lsp_show_json(context: Context, name: str) -> None:
|
|
context.lsp_cli_result = _runner.invoke(app, ["show", name, "--format", "json"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps — helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call _reset_registry and then _get_registry")
|
|
def step_reset_and_get_registry(context: Context) -> None:
|
|
_reset_registry()
|
|
context.lsp_new_registry = _get_registry()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the lsp CLI command should succeed")
|
|
def step_lsp_cli_success(context: Context) -> None:
|
|
result = context.lsp_cli_result
|
|
assert result is not None, "No CLI result captured"
|
|
assert result.exit_code == 0, (
|
|
f"Expected exit code 0, got {result.exit_code}. Output:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then("the lsp CLI command should abort")
|
|
def step_lsp_cli_abort(context: Context) -> None:
|
|
result = context.lsp_cli_result
|
|
assert result is not None, "No CLI result captured"
|
|
assert result.exit_code != 0, (
|
|
f"Expected non-zero exit code, got {result.exit_code}. Output:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then('the lsp CLI output should contain "{text}"')
|
|
def step_lsp_cli_output_contains(context: Context, text: str) -> None:
|
|
result = context.lsp_cli_result
|
|
assert result is not None, "No CLI result captured"
|
|
assert text in result.output, f"Expected '{text}' in output. Got:\n{result.output}"
|
|
|
|
|
|
@then('the lsp CLI output should not contain "{text}"')
|
|
def step_lsp_cli_output_not_contains(context: Context, text: str) -> None:
|
|
result = context.lsp_cli_result
|
|
assert result is not None, "No CLI result captured"
|
|
assert text not in result.output, (
|
|
f"Did not expect '{text}' in output. Got:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then("the lsp CLI output should be valid JSON")
|
|
def step_lsp_cli_output_valid_json(context: Context) -> None:
|
|
result = context.lsp_cli_result
|
|
assert result is not None, "No CLI result captured"
|
|
try:
|
|
parsed = json.loads(result.output)
|
|
assert parsed is not None
|
|
except json.JSONDecodeError as exc:
|
|
raise AssertionError(
|
|
f"Output is not valid JSON: {exc}\nOutput:\n{result.output}"
|
|
) from exc
|
|
|
|
|
|
@then("a new LspRegistry instance should be returned")
|
|
def step_new_registry_instance(context: Context) -> None:
|
|
assert context.lsp_new_registry is not None
|
|
assert isinstance(context.lsp_new_registry, LspRegistry)
|
|
|
|
|
|
@then("the new registry should be empty")
|
|
def step_new_registry_empty(context: Context) -> None:
|
|
assert len(context.lsp_new_registry) == 0, (
|
|
f"Expected empty registry, got {len(context.lsp_new_registry)} servers"
|
|
)
|