forked from HAL9000/cleveragents-core
a808c395f9
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
459 lines
15 KiB
Python
459 lines
15 KiB
Python
"""Behave step implementations for UKO loader coverage-boost scenarios.
|
|
|
|
Targets uncovered lines in src/cleveragents/application/services/uko_loader.py:
|
|
87-90, 101-102, 181, 249-259, 310, 361, 363, 370, 443, 451, 457, 469, 472,
|
|
517, 522, 531.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.uko_loader import (
|
|
UKOLoader,
|
|
UKOValidationError,
|
|
_detect_layer,
|
|
_extract_iri,
|
|
)
|
|
from cleveragents.domain.models.core.uko import (
|
|
UKONode,
|
|
UKOOntology,
|
|
UKOPrefix,
|
|
UKOVersion,
|
|
)
|
|
|
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_version(ver: str = "0.1.0") -> UKOVersion:
|
|
return UKOVersion(
|
|
version_iri=f"https://cleveragents.ai/ontology/uko/{ver}",
|
|
version=ver,
|
|
label="Test",
|
|
comment="Test",
|
|
)
|
|
|
|
|
|
def _make_prefix(prefix: str = "uko") -> UKOPrefix:
|
|
return UKOPrefix(
|
|
prefix=prefix,
|
|
uri="https://cleveragents.ai/ontology/uko#",
|
|
)
|
|
|
|
|
|
# ── Given ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@given('a full IRI "{uri}"')
|
|
def step_given_full_iri(context: Context, uri: str) -> None:
|
|
context.uri = uri
|
|
|
|
|
|
@given('a value string "{value}"')
|
|
def step_given_value_string(context: Context, value: str) -> None:
|
|
context.value_string = value
|
|
|
|
|
|
@given("a TTL string with a property whose value starts with http:")
|
|
@given("a TTL string with a property whose value starts with http::")
|
|
def step_given_ttl_http_property(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a UKO ontology with a prefixed parent referencing an undefined prefix")
|
|
def step_given_undefined_prefix_parent(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#Child",
|
|
rdf_type="owl:Class",
|
|
label="Child",
|
|
parent_uris=("nope:DoesNotExist",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology with a prefixed parent referencing a non-existent node")
|
|
def step_given_nonexistent_prefixed_parent(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#Child",
|
|
rdf_type="owl:Class",
|
|
label="Child",
|
|
parent_uris=("uko:Ghost",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology where a node has an external parent URI")
|
|
def step_given_external_parent(context: Context) -> None:
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri="https://cleveragents.ai/ontology/uko#Leaf",
|
|
rdf_type="owl:Class",
|
|
label="Leaf",
|
|
# Parent points to a URI that does NOT exist in nodes
|
|
parent_uris=("https://example.org/external#Root",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology with diamond inheritance shape")
|
|
def step_given_diamond(context: Context) -> None:
|
|
"""Diamond: D -> B, D -> C, B -> A, C -> A."""
|
|
ns = "https://cleveragents.ai/ontology/uko#"
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri=f"{ns}A",
|
|
rdf_type="owl:Class",
|
|
label="A",
|
|
layer=0,
|
|
),
|
|
UKONode(
|
|
uri=f"{ns}B",
|
|
rdf_type="owl:Class",
|
|
label="B",
|
|
parent_uris=(f"{ns}A",),
|
|
layer=0,
|
|
),
|
|
UKONode(
|
|
uri=f"{ns}C",
|
|
rdf_type="owl:Class",
|
|
label="C",
|
|
parent_uris=(f"{ns}A",),
|
|
layer=0,
|
|
),
|
|
UKONode(
|
|
uri=f"{ns}D",
|
|
rdf_type="owl:Class",
|
|
label="D",
|
|
parent_uris=(f"{ns}B", f"{ns}C"),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology where a node is its own parent")
|
|
def step_given_self_cycle(context: Context) -> None:
|
|
ns = "https://cleveragents.ai/ontology/uko#"
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri=f"{ns}SelfRef",
|
|
rdf_type="owl:Class",
|
|
label="SelfRef",
|
|
parent_uris=(f"{ns}SelfRef",),
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a UKO ontology with some nodes")
|
|
def step_given_some_nodes(context: Context) -> None:
|
|
ns = "https://cleveragents.ai/ontology/uko#"
|
|
context.ontology = UKOOntology(
|
|
version=_make_version(),
|
|
prefixes=(_make_prefix("uko"),),
|
|
nodes=(
|
|
UKONode(
|
|
uri=f"{ns}Existing",
|
|
rdf_type="owl:Class",
|
|
label="Existing",
|
|
layer=0,
|
|
),
|
|
),
|
|
)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a TTL string with dots in bare subject names:")
|
|
def step_given_ttl_dots_in_bare_names(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a TTL string without trailing period:")
|
|
def step_given_ttl_no_trailing_period(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a TTL string with a stray single token:")
|
|
def step_given_ttl_stray_token(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a TTL string with trailing semicolons and bare keywords:")
|
|
def step_given_ttl_trailing_semicolons(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
@given("a UKO loader instance")
|
|
def step_given_loader_instance(context: Context) -> None:
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a temporary TTL file with valid content")
|
|
def step_given_temp_ttl_file(context: Context) -> None:
|
|
ttl_content = (
|
|
"@prefix uko: <https://cleveragents.ai/ontology/uko#> .\n"
|
|
"@prefix owl: <http://www.w3.org/2002/07/owl#> .\n"
|
|
"@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n"
|
|
"\n"
|
|
"uko: a owl:Ontology ;\n"
|
|
" owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> ;\n"
|
|
' rdfs:label "Temp" ;\n'
|
|
' rdfs:comment "Temp test" .\n'
|
|
"\n"
|
|
"uko:Widget a owl:Class ;\n"
|
|
' rdfs:label "Widget" .\n'
|
|
)
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
suffix=".ttl",
|
|
delete=False,
|
|
encoding="utf-8",
|
|
) as tmp:
|
|
tmp.write(ttl_content)
|
|
context.ttl_tmp_path = Path(tmp.name)
|
|
context.loader = UKOLoader()
|
|
|
|
|
|
@given("a TTL string using rdf:type for ontology declaration:")
|
|
def step_given_rdf_type_ontology(context: Context) -> None:
|
|
context.ttl_text = context.text
|
|
|
|
|
|
# ── When ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@when("I detect the layer for that URI")
|
|
def step_when_detect_layer(context: Context) -> None:
|
|
context.detected_layer = _detect_layer(context.uri)
|
|
|
|
|
|
@when("I extract the IRI from that value")
|
|
def step_when_extract_iri(context: Context) -> None:
|
|
context.extracted_iri = _extract_iri(context.value_string)
|
|
|
|
|
|
@when("I parse the TTL coverage string")
|
|
def step_when_parse_ttl_coverage(context: Context) -> None:
|
|
loader = UKOLoader()
|
|
context.parsed_ontology = loader.load_from_string(context.ttl_text)
|
|
context.loader = loader
|
|
|
|
|
|
@when("I run validation on the ontology")
|
|
def step_when_run_validation(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
context.validation_errors = loader.validate(context.ontology)
|
|
|
|
|
|
@when("I resolve the inheritance chain for the node with external parent")
|
|
def step_when_resolve_external_parent(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
context.inheritance_chain = loader.resolve_inheritance(
|
|
context.ontology,
|
|
"https://cleveragents.ai/ontology/uko#Leaf",
|
|
)
|
|
|
|
|
|
@when("I resolve the inheritance chain for the diamond leaf")
|
|
def step_when_resolve_diamond_leaf(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
context.inheritance_chain = loader.resolve_inheritance(
|
|
context.ontology,
|
|
"https://cleveragents.ai/ontology/uko#D",
|
|
)
|
|
|
|
|
|
@when("I attempt to resolve inheritance for the self-referencing node")
|
|
def step_when_resolve_self_cycle(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
try:
|
|
loader.resolve_inheritance(
|
|
context.ontology,
|
|
"https://cleveragents.ai/ontology/uko#SelfRef",
|
|
)
|
|
context.cycle_error = None
|
|
except UKOValidationError as exc:
|
|
context.cycle_error = str(exc)
|
|
|
|
|
|
@when("I resolve the inheritance chain for a URI not in the ontology")
|
|
def step_when_resolve_missing_uri(context: Context) -> None:
|
|
loader: UKOLoader = context.loader
|
|
context.inheritance_chain = loader.resolve_inheritance(
|
|
context.ontology,
|
|
"https://cleveragents.ai/ontology/uko#NoSuchNode",
|
|
)
|
|
|
|
|
|
@when('I resolve the prefixed URI "{uri}" with an empty prefix map')
|
|
def step_when_resolve_prefixed_uri(context: Context, uri: str) -> None:
|
|
context.resolved_uri = context.loader._resolve_uri(uri, {})
|
|
|
|
|
|
@when("I extract version string from an empty IRI")
|
|
def step_when_extract_empty_version(context: Context) -> None:
|
|
context.extracted_version = context.loader._extract_version_string("")
|
|
|
|
|
|
@when('I extract version string from "{iri}"')
|
|
def step_when_extract_version(context: Context, iri: str) -> None:
|
|
context.extracted_version = context.loader._extract_version_string(iri)
|
|
|
|
|
|
@when("I load the ontology from the temporary file")
|
|
def step_when_load_from_file(context: Context) -> None:
|
|
context.loaded_ontology = context.loader.load(context.ttl_tmp_path)
|
|
|
|
|
|
# ── Then ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@then("the detected layer should be {layer:d}")
|
|
def step_then_detected_layer(context: Context, layer: int) -> None:
|
|
assert context.detected_layer == layer, (
|
|
f"Expected layer {layer}, got {context.detected_layer}"
|
|
)
|
|
|
|
|
|
@then('the extracted IRI should be "{expected}"')
|
|
def step_then_extracted_iri(context: Context, expected: str) -> None:
|
|
assert context.extracted_iri == expected, (
|
|
f"Expected '{expected}', got '{context.extracted_iri}'"
|
|
)
|
|
|
|
|
|
@then('the parsed ontology should contain a node labeled "{label}"')
|
|
def step_then_parsed_has_node(context: Context, label: str) -> None:
|
|
labels = [n.label for n in context.parsed_ontology.nodes]
|
|
assert label in labels, f"Label '{label}' not found in {labels}"
|
|
|
|
|
|
@then('node "{label}" property "{prop}" should equal "{expected}"')
|
|
def step_then_node_prop_equals(
|
|
context: Context,
|
|
label: str,
|
|
prop: str,
|
|
expected: str,
|
|
) -> None:
|
|
matching = [n for n in context.parsed_ontology.nodes if n.label == label]
|
|
assert matching, f"No node labeled '{label}'"
|
|
prop_dict = dict(matching[0].properties)
|
|
assert prop in prop_dict, (
|
|
f"Property '{prop}' not on '{label}'; have {list(prop_dict)}"
|
|
)
|
|
assert prop_dict[prop] == expected, (
|
|
f"Expected '{expected}', got '{prop_dict[prop]}'"
|
|
)
|
|
|
|
|
|
@then('the validation result should mention "{text}"')
|
|
def step_then_validation_mentions(context: Context, text: str) -> None:
|
|
combined = " ".join(context.validation_errors)
|
|
assert text in combined, f"Expected '{text}' in errors: {context.validation_errors}"
|
|
|
|
|
|
@then("the chain should contain the starting node only")
|
|
def step_then_chain_single(context: Context) -> None:
|
|
assert len(context.inheritance_chain) == 1, (
|
|
f"Expected 1 node, got {len(context.inheritance_chain)}"
|
|
)
|
|
assert context.inheritance_chain[0].label == "Leaf"
|
|
|
|
|
|
@then("the chain should include each ancestor at most once")
|
|
def step_then_chain_no_duplicates(context: Context) -> None:
|
|
uris = [n.uri for n in context.inheritance_chain]
|
|
assert len(uris) == len(set(uris)), f"Duplicate nodes found: {uris}"
|
|
# Diamond D->B,C->A => chain should be D, B, C, A (4 unique)
|
|
assert len(uris) == 4, f"Expected 4 nodes in diamond, got {len(uris)}: {uris}"
|
|
|
|
|
|
@then("a cycle error should be raised for that node")
|
|
def step_then_self_cycle_error(context: Context) -> None:
|
|
assert context.cycle_error is not None, "Expected cycle error but got none"
|
|
assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error, (
|
|
f"Error message does not mention cycle: {context.cycle_error}"
|
|
)
|
|
|
|
|
|
@then("the chain should be empty")
|
|
def step_then_chain_empty(context: Context) -> None:
|
|
assert len(context.inheritance_chain) == 0, (
|
|
f"Expected empty chain, got {len(context.inheritance_chain)}"
|
|
)
|
|
|
|
|
|
@then('the parsed ontology version should be "{version}"')
|
|
def step_then_parsed_version(context: Context, version: str) -> None:
|
|
assert context.parsed_ontology.version.version == version, (
|
|
f"Expected '{version}', got '{context.parsed_ontology.version.version}'"
|
|
)
|
|
|
|
|
|
@then('the resolved URI should be "{expected}"')
|
|
def step_then_resolved_uri(context: Context, expected: str) -> None:
|
|
assert context.resolved_uri == expected, (
|
|
f"Expected '{expected}', got '{context.resolved_uri}'"
|
|
)
|
|
|
|
|
|
@then('the extracted version should be "{expected}"')
|
|
def step_then_extracted_version(context: Context, expected: str) -> None:
|
|
assert context.extracted_version == expected, (
|
|
f"Expected '{expected}', got '{context.extracted_version}'"
|
|
)
|
|
|
|
|
|
@then('the extracted version should be ""')
|
|
def step_then_extracted_version_empty(context: Context) -> None:
|
|
assert context.extracted_version == "", (
|
|
f"Expected empty string, got '{context.extracted_version}'"
|
|
)
|
|
|
|
|
|
@then('the loaded ontology should have version "{version}"')
|
|
def step_then_loaded_version(context: Context, version: str) -> None:
|
|
assert context.loaded_ontology.version.version == version, (
|
|
f"Expected '{version}', got '{context.loaded_ontology.version.version}'"
|
|
)
|
|
|
|
|
|
@then('the parsed ontology version label should be "{label}"')
|
|
def step_then_parsed_version_label(context: Context, label: str) -> None:
|
|
assert context.parsed_ontology.version.label == label, (
|
|
f"Expected '{label}', got '{context.parsed_ontology.version.label}'"
|
|
)
|