"""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: .\n" "@prefix owl: .\n" "@prefix rdfs: .\n" "\n" "uko: a owl:Ontology ;\n" " owl:versionIRI ;\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}'" )