feat(resource): add LSP resource types
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m46s
CI / typecheck (pull_request) Successful in 3m54s
CI / security (pull_request) Successful in 4m3s
CI / unit_tests (pull_request) Successful in 5m57s
CI / integration_tests (pull_request) Successful in 6m45s
CI / docker (pull_request) Successful in 56s
CI / e2e_tests (pull_request) Successful in 8m29s
CI / coverage (pull_request) Successful in 11m12s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Failing after 52m33s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 21s
CI / lint (pull_request) Successful in 3m18s
CI / quality (pull_request) Successful in 3m46s
CI / typecheck (pull_request) Successful in 3m54s
CI / security (pull_request) Successful in 4m3s
CI / unit_tests (pull_request) Successful in 5m57s
CI / integration_tests (pull_request) Successful in 6m45s
CI / docker (pull_request) Successful in 56s
CI / e2e_tests (pull_request) Successful in 8m29s
CI / coverage (pull_request) Successful in 11m12s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Failing after 52m33s
Add 4 LSP-related built-in resource types to the resource registry: - executable: system binary/interpreter/LSP server binary with auto-discovery from container-exec-env and fs-directory (lazy) - lsp-server: LSP server definition with command, language-ids, transport, args, port, initialization-options config; children: lsp-workspace - lsp-workspace: workspace root tracked by LSP server, auto-discovered from lsp-server; children: lsp-document; not user-addable - lsp-document: text document tracked by LSP server, auto-discovered from lsp-workspace; read+write capabilities; not user-addable Type definitions extracted to _resource_registry_lsp.py for consistency with existing type modules. Parent/child hierarchy: lsp-server -> lsp-workspace -> lsp-document. YAML configs with ADR references (ADR-039, ADR-040). All 7 lsp-server CLI args per ADR-040. Behave tests (21 scenarios): YAML loading, user-addable flags, capabilities, parent/child hierarchy, auto-discovery for all 3 discoverable types, BUILTIN_NAMES, DB bootstrap roundtrip, negative tests for manual registration rejection. Robot tests (6 tests): import, BUILTIN_NAMES, DB roundtrip, hierarchy, auto-discovery, user-addable guard. ISSUES CLOSED: #832
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added LSP resource types: `executable`, `lsp-server`, `lsp-workspace`,
|
||||
`lsp-document` with parent/child hierarchy, auto-discovery rules, and
|
||||
handler references. Registered in bootstrap, with YAML configurations,
|
||||
Behave BDD tests (21 scenarios), and Robot integration tests (6 tests). (#832)
|
||||
- Implemented functional LSP runtime replacing local-mode stubs with real
|
||||
LSP protocol support. `StdioTransport` manages server subprocesses via
|
||||
JSON-RPC over stdin/stdout. `LspClient` implements initialize/shutdown/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Spec reference: docs/adr/ADR-039-container-resource-types.md lines 207-226
|
||||
schema_version: "1.0"
|
||||
name: executable
|
||||
description: "System executable (language runtime, compiler, LSP server binary). Discovered from container or host PATH."
|
||||
resource_kind: physical
|
||||
sandbox_strategy: none
|
||||
user_addable: true
|
||||
built_in: true
|
||||
cli_args:
|
||||
- name: path
|
||||
type: string
|
||||
required: true
|
||||
description: Absolute path to the executable
|
||||
- name: version
|
||||
type: string
|
||||
required: false
|
||||
description: Executable version if detectable
|
||||
parent_types: ["container-exec-env", "fs-directory"]
|
||||
child_types: []
|
||||
handler: "cleveragents.resource.handlers.executable:ExecutableHandler"
|
||||
auto_discovery:
|
||||
trigger_types: ["container-exec-env", "fs-directory"]
|
||||
scan_paths: []
|
||||
lazy: true
|
||||
capabilities:
|
||||
read: true
|
||||
write: false
|
||||
sandbox: false
|
||||
checkpoint: false
|
||||
@@ -0,0 +1,20 @@
|
||||
# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 109-131
|
||||
schema_version: "1.0"
|
||||
name: lsp-document
|
||||
description: "Text document tracked by an LSP server. Provides semantic access: types, diagnostics, completions, references, symbols."
|
||||
resource_kind: physical
|
||||
sandbox_strategy: none
|
||||
user_addable: false
|
||||
built_in: true
|
||||
cli_args: []
|
||||
parent_types: ["lsp-workspace"]
|
||||
child_types: []
|
||||
handler: "cleveragents.resource.handlers.lsp:LSPDocumentHandler"
|
||||
auto_discovery:
|
||||
trigger_types: ["lsp-workspace"]
|
||||
scan_paths: []
|
||||
capabilities:
|
||||
read: true
|
||||
write: true
|
||||
sandbox: false
|
||||
checkpoint: false
|
||||
@@ -0,0 +1,45 @@
|
||||
# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 69-88
|
||||
schema_version: "1.0"
|
||||
name: lsp-server
|
||||
description: "LSP server definition and running process. Provides semantic services for one or more languages."
|
||||
resource_kind: physical
|
||||
sandbox_strategy: none
|
||||
user_addable: true
|
||||
built_in: true
|
||||
cli_args:
|
||||
- name: server-name
|
||||
type: string
|
||||
required: true
|
||||
description: Unique server name
|
||||
- name: command
|
||||
type: string
|
||||
required: true
|
||||
description: Path to server executable or command
|
||||
- name: language-ids
|
||||
type: string
|
||||
required: true
|
||||
description: "Comma-separated language identifiers (e.g. python,typescript)"
|
||||
- name: transport
|
||||
type: string
|
||||
required: false
|
||||
description: "Transport protocol: stdio | tcp | pipe (default: stdio)"
|
||||
- name: args
|
||||
type: string
|
||||
required: false
|
||||
description: Additional command-line arguments
|
||||
- name: port
|
||||
type: string
|
||||
required: false
|
||||
description: TCP port (required when transport=tcp)
|
||||
- name: initialization-options
|
||||
type: string
|
||||
required: false
|
||||
description: JSON initialization options for the server
|
||||
parent_types: ["container-instance", "container-exec-env"]
|
||||
child_types: ["lsp-workspace"]
|
||||
handler: "cleveragents.resource.handlers.lsp:LSPServerHandler"
|
||||
capabilities:
|
||||
read: true
|
||||
write: false
|
||||
sandbox: false
|
||||
checkpoint: false
|
||||
@@ -0,0 +1,20 @@
|
||||
# Spec reference: docs/adr/ADR-040-lsp-resource-types.md lines 90-107
|
||||
schema_version: "1.0"
|
||||
name: lsp-workspace
|
||||
description: "Workspace root tracked by an LSP server. Corresponds to a workspaceFolder in the LSP specification."
|
||||
resource_kind: physical
|
||||
sandbox_strategy: none
|
||||
user_addable: false
|
||||
built_in: true
|
||||
cli_args: []
|
||||
parent_types: ["lsp-server"]
|
||||
child_types: ["lsp-document"]
|
||||
handler: "cleveragents.resource.handlers.lsp:LSPWorkspaceHandler"
|
||||
auto_discovery:
|
||||
trigger_types: ["lsp-server"]
|
||||
scan_paths: []
|
||||
capabilities:
|
||||
read: true
|
||||
write: false
|
||||
sandbox: false
|
||||
checkpoint: false
|
||||
@@ -0,0 +1,137 @@
|
||||
@m7 @resource_type @lsp
|
||||
Feature: LSP Resource Types
|
||||
As a developer using language tooling
|
||||
I want LSP-related resource types registered in the resource DAG
|
||||
So that actors can discover and use language servers
|
||||
|
||||
# ── YAML loading ─────────────────────────────────────────────
|
||||
|
||||
Scenario Outline: LSP type <type_name> loads from YAML for lsp_rt
|
||||
Given the built-in <type_name> YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt schema should be valid
|
||||
And the lsp_rt schema name should be "<type_name>"
|
||||
And the lsp_rt schema resource_kind should be "physical"
|
||||
And the lsp_rt schema sandbox_strategy should be "none"
|
||||
|
||||
Examples:
|
||||
| type_name |
|
||||
| executable |
|
||||
| lsp-server |
|
||||
| lsp-workspace |
|
||||
| lsp-document |
|
||||
|
||||
# ── User-addable flags ────────────────────────────────────────
|
||||
|
||||
Scenario: executable is user-addable for lsp_rt
|
||||
Given the built-in executable YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt schema user_addable should be true
|
||||
|
||||
Scenario: lsp-server is user-addable for lsp_rt
|
||||
Given the built-in lsp-server YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt schema user_addable should be true
|
||||
|
||||
Scenario: lsp-workspace is not user-addable for lsp_rt
|
||||
Given the built-in lsp-workspace YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt schema user_addable should be false
|
||||
|
||||
Scenario: lsp-document is not user-addable for lsp_rt
|
||||
Given the built-in lsp-document YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt schema user_addable should be false
|
||||
|
||||
# ── Capabilities ──────────────────────────────────────────────
|
||||
|
||||
Scenario: lsp-document has write capability for lsp_rt
|
||||
Given the built-in lsp-document YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt capability read should be true
|
||||
And the lsp_rt capability write should be true
|
||||
|
||||
Scenario: executable has read-only capabilities for lsp_rt
|
||||
Given the built-in executable YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt capability read should be true
|
||||
And the lsp_rt capability write should be false
|
||||
|
||||
# ── Parent/child hierarchy ────────────────────────────────────
|
||||
|
||||
Scenario: lsp-server has lsp-workspace as child type for lsp_rt
|
||||
Given the built-in lsp-server YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt child_types should contain "lsp-workspace"
|
||||
|
||||
Scenario: lsp-workspace has lsp-document as child type for lsp_rt
|
||||
Given the built-in lsp-workspace YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt child_types should contain "lsp-document"
|
||||
|
||||
Scenario: lsp-workspace has lsp-server as parent type for lsp_rt
|
||||
Given the built-in lsp-workspace YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt parent_types should contain "lsp-server"
|
||||
|
||||
Scenario: lsp-document has lsp-workspace as parent type for lsp_rt
|
||||
Given the built-in lsp-document YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt parent_types should contain "lsp-workspace"
|
||||
|
||||
# ── Auto-discovery ────────────────────────────────────────────
|
||||
|
||||
Scenario: executable has auto-discovery from container-exec-env for lsp_rt
|
||||
Given the built-in executable YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt auto_discovery should be present
|
||||
And the lsp_rt auto_discovery trigger_types should contain "container-exec-env"
|
||||
|
||||
Scenario: lsp-workspace has auto-discovery from lsp-server for lsp_rt
|
||||
Given the built-in lsp-workspace YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt auto_discovery should be present
|
||||
And the lsp_rt auto_discovery trigger_types should contain "lsp-server"
|
||||
|
||||
# ── BUILTIN_NAMES ─────────────────────────────────────────────
|
||||
|
||||
Scenario: BUILTIN_NAMES includes all LSP types for lsp_rt
|
||||
Given the ResourceTypeSpec BUILTIN_NAMES set for lsp_rt
|
||||
Then BUILTIN_NAMES should contain "executable" for lsp_rt
|
||||
And BUILTIN_NAMES should contain "lsp-server" for lsp_rt
|
||||
And BUILTIN_NAMES should contain "lsp-workspace" for lsp_rt
|
||||
And BUILTIN_NAMES should contain "lsp-document" for lsp_rt
|
||||
|
||||
# ── Bootstrap DB roundtrip ────────────────────────────────────
|
||||
|
||||
Scenario: LSP types survive DB roundtrip after bootstrap for lsp_rt
|
||||
Given a lsp_rt fresh in-memory resource registry with bootstrap
|
||||
When I query the lsp_rt registry for type "executable"
|
||||
Then the lsp_rt db type "executable" should exist
|
||||
And the lsp_rt db type "executable" should have kind "physical"
|
||||
When I query the lsp_rt registry for type "lsp-server"
|
||||
Then the lsp_rt db type "lsp-server" should exist
|
||||
When I query the lsp_rt registry for type "lsp-workspace"
|
||||
Then the lsp_rt db type "lsp-workspace" should exist
|
||||
When I query the lsp_rt registry for type "lsp-document"
|
||||
Then the lsp_rt db type "lsp-document" should exist
|
||||
|
||||
# ── Auto-discovery: lsp-document ──────────────────────────────
|
||||
|
||||
Scenario: lsp-document has auto-discovery from lsp-workspace for lsp_rt
|
||||
Given the built-in lsp-document YAML file for lsp_rt
|
||||
When I load the YAML via schema for lsp_rt
|
||||
Then the lsp_rt auto_discovery should be present
|
||||
And the lsp_rt auto_discovery trigger_types should contain "lsp-workspace"
|
||||
|
||||
# ── Negative tests ────────────────────────────────────────────
|
||||
|
||||
Scenario: lsp-workspace rejects manual registration for lsp_rt
|
||||
Given a lsp_rt fresh in-memory resource registry with bootstrap
|
||||
When I attempt to manually register "lsp-workspace" for lsp_rt
|
||||
Then the lsp_rt manual registration should be rejected
|
||||
|
||||
Scenario: lsp-document rejects manual registration for lsp_rt
|
||||
Given a lsp_rt fresh in-memory resource registry with bootstrap
|
||||
When I attempt to manually register "lsp-document" for lsp_rt
|
||||
Then the lsp_rt manual registration should be rejected
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Step definitions for resource_type_lsp.feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
|
||||
from cleveragents.resource.schema import ResourceTypeConfigSchema
|
||||
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parents[2] / "examples" / "resource-types"
|
||||
|
||||
|
||||
# ── Given steps ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("the built-in {type_name} YAML file for lsp_rt")
|
||||
def step_load_yaml_file_lsp_rt(context: object, type_name: str) -> None:
|
||||
"""Load the built-in YAML file for the given type."""
|
||||
yaml_path = _EXAMPLES_DIR / f"{type_name}.yaml"
|
||||
assert yaml_path.exists(), f"YAML file not found: {yaml_path}"
|
||||
with yaml_path.open() as f:
|
||||
context.lsp_rt_config = yaml.safe_load(f) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("the ResourceTypeSpec BUILTIN_NAMES set for lsp_rt")
|
||||
def step_load_builtin_names_lsp_rt(context: object) -> None:
|
||||
"""Load the BUILTIN_NAMES frozenset."""
|
||||
context.lsp_rt_builtin_names = ResourceTypeSpec.BUILTIN_NAMES # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@given("a lsp_rt fresh in-memory resource registry with bootstrap")
|
||||
def step_fresh_registry_lsp_rt(context: object) -> None:
|
||||
"""Create an in-memory SQLite DB, bootstrap all built-in types."""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.application.services.resource_registry_service import (
|
||||
ResourceRegistryService,
|
||||
)
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
context.lsp_rt_service = ResourceRegistryService( # type: ignore[attr-defined]
|
||||
session_factory=factory,
|
||||
)
|
||||
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@when("I load the YAML via schema for lsp_rt")
|
||||
def step_load_schema_lsp_rt(context: object) -> None:
|
||||
"""Parse the YAML config into a ResourceTypeConfigSchema."""
|
||||
config = context.lsp_rt_config # type: ignore[attr-defined]
|
||||
context.lsp_rt_schema = ResourceTypeConfigSchema(**config) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@when('I query the lsp_rt registry for type "{name}"')
|
||||
def step_query_registry_lsp_rt(context: object, name: str) -> None:
|
||||
"""Query the bootstrapped registry for a specific type."""
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
|
||||
service = context.lsp_rt_service # type: ignore[attr-defined]
|
||||
try:
|
||||
context.lsp_rt_db_spec = service.show_type(name) # type: ignore[attr-defined]
|
||||
context.lsp_rt_db_found = True # type: ignore[attr-defined]
|
||||
except NotFoundError:
|
||||
context.lsp_rt_db_spec = None # type: ignore[attr-defined]
|
||||
context.lsp_rt_db_found = False # type: ignore[attr-defined]
|
||||
|
||||
|
||||
# ── Then steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the lsp_rt schema should be valid")
|
||||
def step_schema_valid_lsp_rt(context: object) -> None:
|
||||
"""Assert the schema loaded successfully."""
|
||||
schema = context.lsp_rt_schema # type: ignore[attr-defined]
|
||||
assert schema is not None, "Schema should not be None"
|
||||
assert schema.name, "Schema must have a non-empty name"
|
||||
|
||||
|
||||
@then('the lsp_rt schema name should be "{name}"')
|
||||
def step_schema_name_lsp_rt(context: object, name: str) -> None:
|
||||
"""Assert the schema name matches."""
|
||||
actual = context.lsp_rt_schema.name # type: ignore[attr-defined]
|
||||
assert actual == name, f"Expected name '{name}', got '{actual}'"
|
||||
|
||||
|
||||
@then('the lsp_rt schema resource_kind should be "{kind}"')
|
||||
def step_schema_kind_lsp_rt(context: object, kind: str) -> None:
|
||||
"""Assert the resource kind matches."""
|
||||
actual = context.lsp_rt_schema.resource_kind # type: ignore[attr-defined]
|
||||
assert actual == kind, f"Expected kind '{kind}', got '{actual}'"
|
||||
|
||||
|
||||
@then('the lsp_rt schema sandbox_strategy should be "{strategy}"')
|
||||
def step_schema_sandbox_lsp_rt(context: object, strategy: str) -> None:
|
||||
"""Assert the sandbox strategy matches."""
|
||||
actual = context.lsp_rt_schema.sandbox_strategy # type: ignore[attr-defined]
|
||||
assert actual == strategy, f"Expected strategy '{strategy}', got '{actual}'"
|
||||
|
||||
|
||||
@then("the lsp_rt schema user_addable should be true")
|
||||
def step_user_addable_true_lsp_rt(context: object) -> None:
|
||||
"""Assert user_addable is True."""
|
||||
actual = context.lsp_rt_schema.user_addable # type: ignore[attr-defined]
|
||||
assert actual is True, f"Expected user_addable True, got {actual}"
|
||||
|
||||
|
||||
@then("the lsp_rt schema user_addable should be false")
|
||||
def step_user_addable_false_lsp_rt(context: object) -> None:
|
||||
"""Assert user_addable is False."""
|
||||
actual = context.lsp_rt_schema.user_addable # type: ignore[attr-defined]
|
||||
assert actual is False, f"Expected user_addable False, got {actual}"
|
||||
|
||||
|
||||
@then("the lsp_rt capability read should be true")
|
||||
def step_cap_read_true_lsp_rt(context: object) -> None:
|
||||
"""Assert read capability is True."""
|
||||
caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined]
|
||||
assert caps["read"] is True, f"Expected read=True, got {caps['read']}"
|
||||
|
||||
|
||||
@then("the lsp_rt capability write should be true")
|
||||
def step_cap_write_true_lsp_rt(context: object) -> None:
|
||||
"""Assert write capability is True."""
|
||||
caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined]
|
||||
assert caps["write"] is True, f"Expected write=True, got {caps['write']}"
|
||||
|
||||
|
||||
@then("the lsp_rt capability write should be false")
|
||||
def step_cap_write_false_lsp_rt(context: object) -> None:
|
||||
"""Assert write capability is False."""
|
||||
caps = context.lsp_rt_schema.capabilities # type: ignore[attr-defined]
|
||||
assert caps["write"] is False, f"Expected write=False, got {caps['write']}"
|
||||
|
||||
|
||||
@then('the lsp_rt child_types should contain "{child}"')
|
||||
def step_child_types_lsp_rt(context: object, child: str) -> None:
|
||||
"""Assert child_types contains the given type."""
|
||||
types = context.lsp_rt_schema.child_types # type: ignore[attr-defined]
|
||||
assert child in types, f"Expected '{child}' in child_types {types}"
|
||||
|
||||
|
||||
@then('the lsp_rt parent_types should contain "{parent}"')
|
||||
def step_parent_types_lsp_rt(context: object, parent: str) -> None:
|
||||
"""Assert parent_types contains the given type."""
|
||||
types = context.lsp_rt_schema.parent_types # type: ignore[attr-defined]
|
||||
assert parent in types, f"Expected '{parent}' in parent_types {types}"
|
||||
|
||||
|
||||
@then("the lsp_rt auto_discovery should be present")
|
||||
def step_auto_discovery_present_lsp_rt(context: object) -> None:
|
||||
"""Assert auto_discovery is present."""
|
||||
ad = context.lsp_rt_schema.auto_discovery # type: ignore[attr-defined]
|
||||
assert ad is not None, "auto_discovery should not be None"
|
||||
|
||||
|
||||
@then('the lsp_rt auto_discovery trigger_types should contain "{trigger}"')
|
||||
def step_auto_discovery_trigger_lsp_rt(context: object, trigger: str) -> None:
|
||||
"""Assert auto_discovery trigger_types contains the given type."""
|
||||
ad = context.lsp_rt_schema.auto_discovery # type: ignore[attr-defined]
|
||||
triggers = ad.get("trigger_types", [])
|
||||
assert trigger in triggers, f"Expected '{trigger}' in trigger_types {triggers}"
|
||||
|
||||
|
||||
@then('BUILTIN_NAMES should contain "{name}" for lsp_rt')
|
||||
def step_builtin_names_contains_lsp_rt(context: object, name: str) -> None:
|
||||
"""Assert BUILTIN_NAMES contains the given name."""
|
||||
names = context.lsp_rt_builtin_names # type: ignore[attr-defined]
|
||||
assert name in names, f"'{name}' not in BUILTIN_NAMES: {sorted(names)}"
|
||||
|
||||
|
||||
@then('the lsp_rt db type "{name}" should exist')
|
||||
def step_db_type_exists_lsp_rt(context: object, name: str) -> None:
|
||||
"""Assert the type was found in the DB."""
|
||||
assert context.lsp_rt_db_found is True, ( # type: ignore[attr-defined]
|
||||
f"Type '{name}' not found after bootstrap"
|
||||
)
|
||||
|
||||
|
||||
@then('the lsp_rt db type "{name}" should have kind "{kind}"')
|
||||
def step_db_type_kind_lsp_rt(context: object, name: str, kind: str) -> None:
|
||||
"""Assert the DB type has the expected resource kind."""
|
||||
spec = context.lsp_rt_db_spec # type: ignore[attr-defined]
|
||||
actual = (
|
||||
str(spec.resource_kind.value)
|
||||
if hasattr(spec.resource_kind, "value")
|
||||
else str(spec.resource_kind)
|
||||
)
|
||||
assert actual == kind, f"Expected '{name}' kind '{kind}', got '{actual}'"
|
||||
|
||||
|
||||
# ── When/Then steps: negative tests ─────────────────────────
|
||||
|
||||
|
||||
@when('I attempt to manually register "{type_name}" for lsp_rt')
|
||||
def step_attempt_manual_register_lsp_rt(context: object, type_name: str) -> None:
|
||||
"""Attempt to register a resource instance of a non-user-addable type."""
|
||||
service = context.lsp_rt_service # type: ignore[attr-defined]
|
||||
context.lsp_rt_register_error = None # type: ignore[attr-defined]
|
||||
try:
|
||||
service.register_resource(
|
||||
type_name=type_name,
|
||||
name=f"test-manual-{type_name}",
|
||||
)
|
||||
except Exception as exc:
|
||||
context.lsp_rt_register_error = exc # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@then("the lsp_rt manual registration should be rejected")
|
||||
def step_manual_registration_rejected_lsp_rt(context: object) -> None:
|
||||
"""Assert that manual registration was rejected."""
|
||||
err = context.lsp_rt_register_error # type: ignore[attr-defined]
|
||||
assert err is not None, "Expected registration to be rejected but it succeeded"
|
||||
assert "not user-addable" in str(err), (
|
||||
f"Expected 'not user-addable' in error, got: {err}"
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Helper for LSP resource type Robot integration tests.
|
||||
|
||||
Issue: #832
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _import_lsp_types() -> None:
|
||||
"""Verify LSP_RESOURCE_TYPES can be imported."""
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
|
||||
assert len(LSP_RESOURCE_TYPES) == 4, f"Expected 4, got {len(LSP_RESOURCE_TYPES)}"
|
||||
names = [t["name"] for t in LSP_RESOURCE_TYPES]
|
||||
for expected in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
|
||||
assert expected in names, f"{expected} not in LSP_RESOURCE_TYPES: {names}"
|
||||
|
||||
print("import-lsp-types-ok")
|
||||
|
||||
|
||||
def _check_builtin_names() -> None:
|
||||
"""Verify all 4 LSP types are in BUILTIN_NAMES."""
|
||||
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
|
||||
|
||||
for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
|
||||
assert name in ResourceTypeSpec.BUILTIN_NAMES, f"{name} not in BUILTIN_NAMES"
|
||||
|
||||
print("check-builtin-names-ok")
|
||||
|
||||
|
||||
def _db_roundtrip() -> None:
|
||||
"""Bootstrap in-memory DB and verify all 4 LSP types are retrievable."""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.application.services.resource_registry_service import (
|
||||
ResourceRegistryService,
|
||||
)
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
svc = ResourceRegistryService(session_factory=factory)
|
||||
|
||||
for name in ("executable", "lsp-server", "lsp-workspace", "lsp-document"):
|
||||
spec = svc.show_type(name)
|
||||
assert spec is not None, f"{name} not found after bootstrap"
|
||||
assert str(spec.resource_kind) == "physical", (
|
||||
f"{name} kind: {spec.resource_kind}"
|
||||
)
|
||||
assert str(spec.sandbox_strategy) == "none", (
|
||||
f"{name} strategy: {spec.sandbox_strategy}"
|
||||
)
|
||||
|
||||
print("db-roundtrip-ok")
|
||||
|
||||
|
||||
def _check_hierarchy() -> None:
|
||||
"""Verify parent/child relationships: server->workspace->document."""
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
|
||||
types = {t["name"]: t for t in LSP_RESOURCE_TYPES}
|
||||
|
||||
# lsp-server has lsp-workspace as child
|
||||
assert "lsp-workspace" in types["lsp-server"]["child_types"], (
|
||||
"lsp-server missing lsp-workspace child"
|
||||
)
|
||||
# lsp-workspace has lsp-server as parent
|
||||
assert "lsp-server" in types["lsp-workspace"]["parent_types"], (
|
||||
"lsp-workspace missing lsp-server parent"
|
||||
)
|
||||
# lsp-workspace has lsp-document as child
|
||||
assert "lsp-document" in types["lsp-workspace"]["child_types"], (
|
||||
"lsp-workspace missing lsp-document child"
|
||||
)
|
||||
# lsp-document has lsp-workspace as parent
|
||||
assert "lsp-workspace" in types["lsp-document"]["parent_types"], (
|
||||
"lsp-document missing lsp-workspace parent"
|
||||
)
|
||||
# executable has no children
|
||||
assert types["executable"]["child_types"] == [], (
|
||||
"executable should have no children"
|
||||
)
|
||||
|
||||
print("check-hierarchy-ok")
|
||||
|
||||
|
||||
def _check_auto_discovery() -> None:
|
||||
"""Verify executable has auto_discovery with trigger_types."""
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
|
||||
types = {t["name"]: t for t in LSP_RESOURCE_TYPES}
|
||||
|
||||
# executable has auto_discovery
|
||||
ad = types["executable"].get("auto_discovery")
|
||||
assert ad is not None, "executable missing auto_discovery"
|
||||
assert "container-exec-env" in ad.get("trigger_types", []), (
|
||||
"executable missing container-exec-env trigger"
|
||||
)
|
||||
assert ad.get("lazy") is True, "executable auto_discovery should be lazy"
|
||||
|
||||
# lsp-workspace has auto_discovery from lsp-server
|
||||
ad_ws = types["lsp-workspace"].get("auto_discovery")
|
||||
assert ad_ws is not None, "lsp-workspace missing auto_discovery"
|
||||
assert "lsp-server" in ad_ws.get("trigger_types", []), (
|
||||
"lsp-workspace missing lsp-server trigger"
|
||||
)
|
||||
|
||||
print("check-auto-discovery-ok")
|
||||
|
||||
|
||||
def _check_user_addable_guard() -> None:
|
||||
"""Verify lsp-workspace and lsp-document reject manual registration."""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.application.services.resource_registry_service import (
|
||||
ResourceRegistryService,
|
||||
)
|
||||
from cleveragents.infrastructure.database.models import Base
|
||||
|
||||
engine = create_engine("sqlite:///:memory:", echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
svc = ResourceRegistryService(session_factory=factory)
|
||||
|
||||
for non_addable in ("lsp-workspace", "lsp-document"):
|
||||
try:
|
||||
svc.register_resource(type_name=non_addable, name=f"test-{non_addable}")
|
||||
print(f"FAIL: {non_addable} registration should have been rejected")
|
||||
sys.exit(1)
|
||||
except Exception as exc:
|
||||
assert "not user-addable" in str(exc), (
|
||||
f"{non_addable}: expected 'not user-addable' in error, got: {exc}"
|
||||
)
|
||||
|
||||
print("check-user-addable-guard-ok")
|
||||
|
||||
|
||||
_COMMANDS: dict[str, Any] = {
|
||||
"import-lsp-types": _import_lsp_types,
|
||||
"check-builtin-names": _check_builtin_names,
|
||||
"db-roundtrip": _db_roundtrip,
|
||||
"check-hierarchy": _check_hierarchy,
|
||||
"check-auto-discovery": _check_auto_discovery,
|
||||
"check-user-addable-guard": _check_user_addable_guard,
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <command>")
|
||||
print(f"Commands: {', '.join(sorted(_COMMANDS))}")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
fn = _COMMANDS.get(cmd)
|
||||
if fn is None:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
fn()
|
||||
except Exception as exc:
|
||||
print(f"FAIL [{cmd}]: {type(exc).__name__}: {exc}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,54 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for LSP resource types: executable,
|
||||
... lsp-server, lsp-workspace, lsp-document.
|
||||
... Spec reference: ADR-039, ADR-040
|
||||
... Issue: #832
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} robot/helper_resource_type_lsp.py
|
||||
|
||||
*** Test Cases ***
|
||||
LSP Resource Types Are Importable
|
||||
[Documentation] Verify LSP_RESOURCE_TYPES can be imported from the data module
|
||||
[Tags] lsp resource import
|
||||
${result}= Run Process ${PYTHON} ${HELPER} import-lsp-types cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Import failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} import-lsp-types-ok
|
||||
|
||||
LSP Types Present In BUILTIN_NAMES
|
||||
[Documentation] Verify all 4 LSP types are in BUILTIN_NAMES
|
||||
[Tags] lsp resource builtin
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-builtin-names cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 BUILTIN_NAMES check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-builtin-names-ok
|
||||
|
||||
LSP Types Survive DB Bootstrap Roundtrip
|
||||
[Documentation] Bootstrap in-memory DB and verify all 4 LSP types are retrievable
|
||||
[Tags] lsp resource bootstrap database
|
||||
${result}= Run Process ${PYTHON} ${HELPER} db-roundtrip cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 DB roundtrip failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} db-roundtrip-ok
|
||||
|
||||
LSP Type Hierarchy Is Correct
|
||||
[Documentation] Verify parent/child relationships: server->workspace->document
|
||||
[Tags] lsp resource hierarchy
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-hierarchy cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Hierarchy check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-hierarchy-ok
|
||||
|
||||
Executable Has Auto-Discovery Configuration
|
||||
[Documentation] Verify executable type has auto_discovery with trigger_types
|
||||
[Tags] lsp resource discovery
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-auto-discovery cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 Discovery check failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-auto-discovery-ok
|
||||
|
||||
Non-User-Addable LSP Types Reject Manual Registration
|
||||
[Documentation] Verify lsp-workspace and lsp-document reject manual registration
|
||||
[Tags] lsp resource guard
|
||||
${result}= Run Process ${PYTHON} ${HELPER} check-user-addable-guard cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0 User-addable guard failed: ${result.stderr}
|
||||
Should Contain ${result.stdout} check-user-addable-guard-ok
|
||||
@@ -22,6 +22,9 @@ from cleveragents.application.services._resource_registry_cloud import (
|
||||
CLOUD_BASE_TYPES,
|
||||
GCP_AZURE_TYPES,
|
||||
)
|
||||
from cleveragents.application.services._resource_registry_lsp import (
|
||||
LSP_RESOURCE_TYPES,
|
||||
)
|
||||
from cleveragents.application.services._resource_registry_physical import (
|
||||
DEFERRED_PHYSICAL_TYPES,
|
||||
)
|
||||
@@ -253,6 +256,8 @@ BUILTIN_TYPES: list[dict[str, Any]] = [
|
||||
*VIRTUAL_CORE_TYPES,
|
||||
# Deferred virtual types -- _resource_registry_virtual_deferred (#331)
|
||||
*VIRTUAL_DEFERRED_TYPES,
|
||||
# LSP resource types -- _resource_registry_lsp (#832)
|
||||
*LSP_RESOURCE_TYPES,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"""LSP resource type definitions for the Resource Registry.
|
||||
|
||||
Contains the built-in LSP-related type entries (``executable``,
|
||||
``lsp-server``, ``lsp-workspace``, ``lsp-document``) that are appended
|
||||
to ``BUILTIN_TYPES`` in ``_resource_registry_data``.
|
||||
|
||||
These types bridge the resource system with the LSP runtime, allowing
|
||||
actors to discover available language tooling and the LSP system to know
|
||||
which resources it operates on.
|
||||
|
||||
Spec reference: docs/adr/ADR-039-container-resource-types.md (executable),
|
||||
docs/adr/ADR-040-lsp-resource-types.md (lsp-server,
|
||||
lsp-workspace, lsp-document)
|
||||
|
||||
.. note::
|
||||
|
||||
This is an **internal** module (``_``-prefixed). External code
|
||||
should import ``BUILTIN_TYPES`` from ``_resource_registry_data``
|
||||
instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["LSP_RESOURCE_TYPES"]
|
||||
|
||||
# === LSP Resource Types (#832) ===
|
||||
#
|
||||
# These types model language tooling infrastructure in the resource DAG.
|
||||
# - executable: system binary / interpreter / LSP server binary
|
||||
# - lsp-server: a configured or running LSP server instance
|
||||
# - lsp-workspace: workspace root tracked by an LSP server
|
||||
# - lsp-document: text document tracked by an LSP server
|
||||
|
||||
LSP_RESOURCE_TYPES: list[dict[str, Any]] = [
|
||||
# ── executable ───────────────────────────────────────────────
|
||||
{
|
||||
"name": "executable",
|
||||
"description": (
|
||||
"System executable (language runtime, compiler, LSP server "
|
||||
"binary). Discovered from container or host PATH."
|
||||
),
|
||||
"resource_kind": "physical",
|
||||
"sandbox_strategy": "none",
|
||||
"user_addable": True,
|
||||
"built_in": True,
|
||||
"cli_args": [
|
||||
{
|
||||
"name": "path",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Absolute path to the executable",
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": "Executable version if detectable",
|
||||
},
|
||||
],
|
||||
"parent_types": ["container-exec-env", "fs-directory"],
|
||||
"child_types": [],
|
||||
"handler": "cleveragents.resource.handlers.executable:ExecutableHandler",
|
||||
"auto_discovery": {
|
||||
"trigger_types": ["container-exec-env", "fs-directory"],
|
||||
"scan_paths": [],
|
||||
"lazy": True,
|
||||
},
|
||||
"capabilities": {
|
||||
"read": True,
|
||||
"write": False,
|
||||
"sandbox": False,
|
||||
"checkpoint": False,
|
||||
},
|
||||
},
|
||||
# ── lsp-server ───────────────────────────────────────────────
|
||||
{
|
||||
"name": "lsp-server",
|
||||
"description": (
|
||||
"LSP server definition and running process. Provides "
|
||||
"semantic services for one or more languages."
|
||||
),
|
||||
"resource_kind": "physical",
|
||||
"sandbox_strategy": "none",
|
||||
"user_addable": True,
|
||||
"built_in": True,
|
||||
"cli_args": [
|
||||
{
|
||||
"name": "server-name",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Unique server name",
|
||||
},
|
||||
{
|
||||
"name": "command",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": "Path to server executable or command",
|
||||
},
|
||||
{
|
||||
"name": "language-ids",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"description": (
|
||||
"Comma-separated language identifiers (e.g. python,typescript)"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "transport",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": ("Transport: stdio | tcp | pipe (default: stdio)"),
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": "Additional command-line arguments",
|
||||
},
|
||||
{
|
||||
"name": "port",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": "TCP port (required when transport=tcp)",
|
||||
},
|
||||
{
|
||||
"name": "initialization-options",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": "JSON initialization options for the server",
|
||||
},
|
||||
],
|
||||
"parent_types": ["container-instance", "container-exec-env"],
|
||||
"child_types": ["lsp-workspace"],
|
||||
"handler": "cleveragents.resource.handlers.lsp:LSPServerHandler",
|
||||
"capabilities": {
|
||||
"read": True,
|
||||
"write": False,
|
||||
"sandbox": False,
|
||||
"checkpoint": False,
|
||||
},
|
||||
},
|
||||
# ── lsp-workspace ────────────────────────────────────────────
|
||||
{
|
||||
"name": "lsp-workspace",
|
||||
"description": (
|
||||
"Workspace root tracked by an LSP server. Corresponds to "
|
||||
"a workspaceFolder in the LSP specification."
|
||||
),
|
||||
"resource_kind": "physical",
|
||||
"sandbox_strategy": "none",
|
||||
"user_addable": False,
|
||||
"built_in": True,
|
||||
"cli_args": [],
|
||||
"parent_types": ["lsp-server"],
|
||||
"child_types": ["lsp-document"],
|
||||
"handler": "cleveragents.resource.handlers.lsp:LSPWorkspaceHandler",
|
||||
"auto_discovery": {
|
||||
"trigger_types": ["lsp-server"],
|
||||
"scan_paths": [],
|
||||
},
|
||||
"capabilities": {
|
||||
"read": True,
|
||||
"write": False,
|
||||
"sandbox": False,
|
||||
"checkpoint": False,
|
||||
},
|
||||
},
|
||||
# ── lsp-document ─────────────────────────────────────────────
|
||||
{
|
||||
"name": "lsp-document",
|
||||
"description": (
|
||||
"Text document tracked by an LSP server. Provides semantic "
|
||||
"access: types, diagnostics, completions, references, symbols."
|
||||
),
|
||||
"resource_kind": "physical",
|
||||
"sandbox_strategy": "none",
|
||||
"user_addable": False,
|
||||
"built_in": True,
|
||||
"cli_args": [],
|
||||
"parent_types": ["lsp-workspace"],
|
||||
"child_types": [],
|
||||
"handler": "cleveragents.resource.handlers.lsp:LSPDocumentHandler",
|
||||
"auto_discovery": {
|
||||
"trigger_types": ["lsp-workspace"],
|
||||
"scan_paths": [],
|
||||
},
|
||||
"capabilities": {
|
||||
"read": True,
|
||||
"write": True,
|
||||
"sandbox": False,
|
||||
"checkpoint": False,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -117,6 +117,11 @@ BUILTIN_TYPE_NAMES: frozenset[str] = frozenset(
|
||||
"remote",
|
||||
"submodule",
|
||||
"symlink",
|
||||
# LSP resource types (#832)
|
||||
"executable",
|
||||
"lsp-server",
|
||||
"lsp-workspace",
|
||||
"lsp-document",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user