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

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:
2026-03-18 04:38:52 +00:00
parent e07b68221a
commit 65a2e4db76
12 changed files with 917 additions and 0 deletions
+4
View File
@@ -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/
+29
View File
@@ -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
+20
View File
@@ -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
+45
View File
@@ -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
+137
View File
@@ -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
+224
View File
@@ -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}"
)
+178
View File
@@ -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)
+54
View File
@@ -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",
}
)