77a0a95dc3
CI / lint (pull_request) Successful in 26s
CI / security (pull_request) Successful in 26s
CI / typecheck (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 17s
CI / build (pull_request) Successful in 25s
CI / integration_tests (pull_request) Failing after 3m2s
CI / lint (push) Successful in 23s
CI / unit_tests (pull_request) Failing after 17m24s
CI / docker (pull_request) Has been skipped
CI / typecheck (push) Successful in 40s
CI / security (push) Successful in 37s
CI / quality (push) Successful in 25s
CI / build (push) Successful in 16s
CI / integration_tests (push) Failing after 7m16s
CI / unit_tests (push) Failing after 24m35s
CI / docker (push) Has been skipped
CI / coverage (pull_request) Failing after 37m0s
CI / coverage (push) Failing after 37m15s
264 lines
8.1 KiB
Python
264 lines
8.1 KiB
Python
"""Step definitions for binding_resolution_coverage.feature.
|
|
|
|
Covers the uncovered lines in binding_resolution_service.py:
|
|
- Static binding with unknown resource (lines 147-148)
|
|
- Parameter binding with unknown ref (lines 198-201)
|
|
- Parameter binding with valid ref (lines 209, 214)
|
|
- Contextual binding, slot not required, no match (line 274)
|
|
- _find_matching_resources skips NotFoundError (lines 298-299)
|
|
- _disambiguate via resource name suffix (lines 325-329)
|
|
- _disambiguate ambiguous raises ValidationError (lines 337-338)
|
|
- _is_type_compatible returns False on NotFoundError (lines 369-370)
|
|
|
|
All Background / shared steps are inherited from binding_resolution_steps.py.
|
|
Only NEW step patterns are defined here to avoid duplication.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.core.exceptions import ValidationError
|
|
from cleveragents.domain.models.core.project import (
|
|
LinkedResource,
|
|
NamespacedProject,
|
|
)
|
|
from cleveragents.domain.models.core.resource import (
|
|
PhysVirt,
|
|
Resource,
|
|
)
|
|
from cleveragents.domain.models.core.tool import (
|
|
BindingMode,
|
|
ResourceAccessMode,
|
|
ResourceSlot,
|
|
Tool,
|
|
ToolSource,
|
|
ToolType,
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _make_resource(
|
|
resource_id: str,
|
|
type_name: str,
|
|
name: str | None = None,
|
|
) -> Resource:
|
|
return Resource(
|
|
resource_id=resource_id,
|
|
name=name,
|
|
resource_type_name=type_name,
|
|
classification=PhysVirt.PHYSICAL,
|
|
description=f"Test resource {resource_id}",
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: named resource in registry (with both id and name keys)
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
'a named resource "{rid}" called "{res_name}" of type "{type_name}" in the registry'
|
|
)
|
|
def step_named_resource_in_registry(
|
|
context: Context,
|
|
rid: str,
|
|
res_name: str,
|
|
type_name: str,
|
|
) -> None:
|
|
resource = _make_resource(rid, type_name, name=res_name)
|
|
context.mock_registry._resources[res_name] = resource
|
|
context.mock_registry._resources[rid] = resource
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: optional contextual slot
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given(
|
|
'a tool "{tool_name}" with an optional contextual slot "{slot}" of type "{rtype}"'
|
|
)
|
|
def step_tool_optional_contextual(
|
|
context: Context,
|
|
tool_name: str,
|
|
slot: str,
|
|
rtype: str,
|
|
) -> None:
|
|
context.tool = Tool(
|
|
name=tool_name,
|
|
description=f"Tool {tool_name}",
|
|
source=ToolSource.BUILTIN,
|
|
tool_type=ToolType.TOOL,
|
|
resource_slots=[
|
|
ResourceSlot(
|
|
name=slot,
|
|
resource_type=rtype,
|
|
access=ResourceAccessMode.READ_WRITE,
|
|
binding=BindingMode.CONTEXTUAL,
|
|
required=False,
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: project with linked resources including missing ones
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given('a project "{name}" with linked resources including a missing one:')
|
|
def step_project_with_missing_resource(
|
|
context: Context,
|
|
name: str,
|
|
) -> None:
|
|
parts = name.split("/", 1)
|
|
namespace = parts[0] if len(parts) == 2 else "local"
|
|
proj_name = parts[1] if len(parts) == 2 else parts[0]
|
|
|
|
links: list[LinkedResource] = []
|
|
for row in context.table:
|
|
rid = row["resource_id"]
|
|
tname = row["type_name"]
|
|
alias = row["alias"] if row["alias"] else None
|
|
exists = row["exists"].lower() == "true"
|
|
|
|
if exists:
|
|
resource = _make_resource(rid, tname, name=None)
|
|
context.mock_registry._resources[rid] = resource
|
|
|
|
# Always create the link, even for non-existent resources
|
|
links.append(
|
|
LinkedResource(
|
|
resource_id=rid,
|
|
alias=alias,
|
|
)
|
|
)
|
|
|
|
context.project = NamespacedProject(
|
|
name=proj_name,
|
|
namespace=namespace,
|
|
linked_resources=links,
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: project with named linked resources (resources have names)
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given('a project "{name}" with named linked resources:')
|
|
def step_project_named_resources(
|
|
context: Context,
|
|
name: str,
|
|
) -> None:
|
|
parts = name.split("/", 1)
|
|
namespace = parts[0] if len(parts) == 2 else "local"
|
|
proj_name = parts[1] if len(parts) == 2 else parts[0]
|
|
|
|
links: list[LinkedResource] = []
|
|
for row in context.table:
|
|
rid = row["resource_id"]
|
|
tname = row["type_name"]
|
|
alias = row["alias"] if row["alias"] else None
|
|
res_name = row["resource_name"] if row["resource_name"] else None
|
|
|
|
resource = _make_resource(rid, tname, name=res_name)
|
|
context.mock_registry._resources[rid] = resource
|
|
|
|
links.append(
|
|
LinkedResource(
|
|
resource_id=rid,
|
|
alias=alias,
|
|
)
|
|
)
|
|
|
|
context.project = NamespacedProject(
|
|
name=proj_name,
|
|
namespace=namespace,
|
|
linked_resources=links,
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: remove resource type from registry
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given('the resource type "{tname}" is removed from the registry')
|
|
def step_remove_resource_type(context: Context, tname: str) -> None:
|
|
context.mock_registry._types.pop(tname, None)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Given: mark slot as required (for post-hoc override)
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@given('the slot "{slot_name}" is required')
|
|
def step_slot_is_required(context: Context, slot_name: str) -> None:
|
|
# The tool already has required=True by default; this is a
|
|
# documentation step that confirms the slot is required.
|
|
for slot in context.tool.resource_slots:
|
|
if slot.name == slot_name:
|
|
assert slot.required, f"Expected slot '{slot_name}' to be required"
|
|
return
|
|
raise AssertionError(f"No slot named '{slot_name}' on tool")
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# When: resolve with invocation params
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@when("the bindings are resolved with invocation params:")
|
|
def step_resolve_with_params(context: Context) -> None:
|
|
params: dict[str, str] = {}
|
|
for row in context.table:
|
|
params[row["slot"]] = row["ref"]
|
|
|
|
try:
|
|
context.binding_results = context.binding_service.resolve(
|
|
context.tool,
|
|
context.project,
|
|
invocation_params=params,
|
|
)
|
|
context.binding_error = None
|
|
except ValidationError as exc:
|
|
context.binding_error = exc
|
|
context.binding_results = None
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Then: check resource_name on binding result
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
@then('the binding for slot "{slot}" should have resource_name "{rname}"')
|
|
def step_check_resource_name(
|
|
context: Context,
|
|
slot: str,
|
|
rname: str,
|
|
) -> None:
|
|
result = _find_result(context, slot)
|
|
assert result.resource_name == rname, (
|
|
f"Expected resource_name '{rname}', got '{result.resource_name}'"
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Internal helper
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _find_result(context: Context, slot_name: str):
|
|
"""Find a binding result by slot name."""
|
|
for result in context.binding_results:
|
|
if result.slot_name == slot_name:
|
|
return result
|
|
raise AssertionError(f"No binding result for slot '{slot_name}'")
|