"""Behave step implementations for UKO ontology feature.""" from __future__ import annotations from pathlib import Path from behave import given, then, when from behave.runner import Context from cleveragents.application.services.uko_loader import ( UKOLoader, UKOValidationError, ) from cleveragents.domain.models.core.uko import ( UKONode, UKOOntology, UKOPrefix, UKOVersion, ) _TTL_FILE = Path(__file__).resolve().parents[2] / "docs" / "ontology" / "uko.ttl" # --------------------------------------------------------------------------- # Given # --------------------------------------------------------------------------- @given("a TTL string") @given("a TTL string:") def step_given_ttl_string(context: Context) -> None: context.ttl_text = context.text @given("the UKO ontology is loaded from the TTL file") def step_given_uko_loaded(context: Context) -> None: loader = UKOLoader() context.ontology = loader.load(_TTL_FILE) context.loader = loader @given("a TTL string with a node missing rdf_type") @given("a TTL string with a node missing rdf_type:") def step_given_ttl_missing_rdf_type(context: Context) -> None: context.ttl_text = context.text @given("a UKO ontology with an undefined prefix node") def step_given_undefined_prefix(context: Context) -> None: context.ontology = UKOOntology( version=UKOVersion( version_iri="https://cleveragents.ai/ontology/uko/0.1.0", version="0.1.0", label="Test", comment="Test", ), prefixes=( UKOPrefix( prefix="uko", uri="https://cleveragents.ai/ontology/uko#", ), ), nodes=( UKONode( uri="badprefix:Thing", rdf_type="owl:Class", label="Thing", layer=0, ), ), ) context.loader = UKOLoader() @given("a UKO ontology with a cycle in inheritance") def step_given_cycle(context: Context) -> None: context.ontology = UKOOntology( version=UKOVersion( version_iri="https://cleveragents.ai/ontology/uko/0.1.0", version="0.1.0", label="Test", comment="Test", ), prefixes=( UKOPrefix( prefix="uko", uri="https://cleveragents.ai/ontology/uko#", ), ), nodes=( UKONode( uri="https://cleveragents.ai/ontology/uko#A", rdf_type="owl:Class", label="A", parent_uris=("https://cleveragents.ai/ontology/uko#B",), layer=0, ), UKONode( uri="https://cleveragents.ai/ontology/uko#B", rdf_type="owl:Class", label="B", parent_uris=("https://cleveragents.ai/ontology/uko#A",), layer=0, ), ), ) context.loader = UKOLoader() @given("a UKO ontology with a node referencing a non-existent parent") def step_given_nonexistent_parent(context: Context) -> None: context.ontology = UKOOntology( version=UKOVersion( version_iri="https://cleveragents.ai/ontology/uko/0.1.0", version="0.1.0", label="Test", comment="Test", ), prefixes=( UKOPrefix( prefix="uko", uri="https://cleveragents.ai/ontology/uko#", ), ), nodes=( UKONode( uri="https://cleveragents.ai/ontology/uko#Orphan", rdf_type="owl:Class", label="Orphan", parent_uris=("https://cleveragents.ai/ontology/uko#DoesNotExist",), layer=0, ), ), ) context.loader = UKOLoader() # --------------------------------------------------------------------------- # When # --------------------------------------------------------------------------- @when("I parse the TTL string") def step_when_parse_ttl(context: Context) -> None: loader = UKOLoader() context.ontology = loader.load_from_string(context.ttl_text) context.loader = loader @when("I get nodes at layer {layer:d}") def step_when_get_layer(context: Context, layer: int) -> None: loader: UKOLoader = getattr(context, "loader", UKOLoader()) context.layer_nodes = loader.get_layer_nodes(context.ontology, layer) @when("I validate the ontology") def step_when_validate(context: Context) -> None: loader: UKOLoader = getattr(context, "loader", UKOLoader()) context.validation_errors = loader.validate(context.ontology) @when('I resolve inheritance for "{uri}"') def step_when_resolve_inheritance(context: Context, uri: str) -> None: loader: UKOLoader = getattr(context, "loader", UKOLoader()) context.inheritance_chain = loader.resolve_inheritance(context.ontology, uri) @when("I parse the TTL string expecting no nodes") def step_when_parse_expecting_no_nodes(context: Context) -> None: loader = UKOLoader() # This TTL has a node without rdf:type — the parser skips it context.ontology = loader.load_from_string(context.ttl_text) context.loader = loader @when("I inject a node without rdf_type into the ontology") def step_inject_typeless_node(context: Context) -> None: typeless = UKONode( uri="https://cleveragents.ai/ontology/uko#Broken", rdf_type="", label="Broken", ) existing = list(context.ontology.nodes) existing.append(typeless) context.ontology = context.ontology.model_copy( update={"nodes": tuple(existing)}, ) @when("I validate the modified ontology") def step_validate_modified(context: Context) -> None: loader = UKOLoader() context.validation_errors = loader.validate(context.ontology) @when("I attempt to resolve inheritance for the cyclic node") def step_when_resolve_cycle(context: Context) -> None: loader: UKOLoader = context.loader try: loader.resolve_inheritance( context.ontology, "https://cleveragents.ai/ontology/uko#A", ) context.cycle_error = None except UKOValidationError as exc: context.cycle_error = str(exc) @when("I attempt to parse the TTL string expecting a validation error") def step_when_parse_expecting_error(context: Context) -> None: loader = UKOLoader() try: loader.load_from_string(context.ttl_text) context.validation_error_msg = None except UKOValidationError as exc: context.validation_error_msg = str(exc) # --------------------------------------------------------------------------- # Then # --------------------------------------------------------------------------- @then('the prefix "{name}" should map to "{uri}"') def step_then_prefix_maps(context: Context, name: str, uri: str) -> None: prefix_map = {p.prefix: p.uri for p in context.ontology.prefixes} assert name in prefix_map, f"Prefix '{name}' not found; have {list(prefix_map)}" assert prefix_map[name] == uri, f"Expected {uri}, got {prefix_map[name]}" @then('I should find a node with label "{label}"') def step_then_find_node(context: Context, label: str) -> None: labels = [n.label for n in context.layer_nodes] assert label in labels, f"Label '{label}' not found in {labels}" @then('the ontology version should be "{version}"') def step_then_version(context: Context, version: str) -> None: assert context.ontology.version.version == version @then('the ontology label should be "{label}"') def step_then_label(context: Context, label: str) -> None: assert context.ontology.version.label == label @then('the ontology comment should be "{comment}"') def step_then_comment(context: Context, comment: str) -> None: assert context.ontology.version.comment == comment @then('the version IRI should be "{iri}"') def step_then_version_iri(context: Context, iri: str) -> None: assert context.ontology.version.version_iri == iri @then("no nodes should be present in the ontology") def step_then_no_nodes(context: Context) -> None: # The "Broken" node has no rdf:type, so the parser skips it broken_nodes = [n for n in context.ontology.nodes if n.label == "Broken"] assert len(broken_nodes) == 0, f"Expected no 'Broken' nodes, found {broken_nodes}" # REMOVED: Duplicate @then step - already defined in subplan_service_coverage_boost_steps.py @then("there should be no validation errors") def step_then_no_errors(context: Context) -> None: assert context.validation_errors == [], ( f"Expected no errors, got: {context.validation_errors}" ) @then("the inheritance chain should have at least {count:d} nodes") def step_then_chain_length(context: Context, count: int) -> None: assert len(context.inheritance_chain) >= count, ( f"Expected >= {count} nodes, got {len(context.inheritance_chain)}" ) @then('the first node in the chain should have label "{label}"') def step_then_first_chain_label(context: Context, label: str) -> None: assert context.inheritance_chain[0].label == label, ( f"Expected '{label}', got '{context.inheritance_chain[0].label}'" ) @then('the last node in the chain should have label "{label}"') def step_then_last_chain_label(context: Context, label: str) -> None: assert context.inheritance_chain[-1].label == label, ( f"Expected '{label}', got '{context.inheritance_chain[-1].label}'" ) @then("there should be at least {count:d} nodes") def step_then_min_nodes(context: Context, count: int) -> None: assert len(context.layer_nodes) >= count, ( f"Expected >= {count}, got {len(context.layer_nodes)}" ) @then("the ontology should have prefixes defined") def step_then_has_prefixes(context: Context) -> None: assert len(context.ontology.prefixes) > 0 @then("the ontology should have nodes defined") def step_then_has_nodes(context: Context) -> None: assert len(context.ontology.nodes) > 0 @then('"{version}" should be in the supported versions') def step_then_supported_version(context: Context, version: str) -> None: assert version in context.ontology.supported_versions @then("the node count should be {count:d}") def step_node_count(context: Context, count: int) -> None: assert len(context.layer_nodes) == count, ( f"Expected {count} nodes but got {len(context.layer_nodes)}" ) @then('validation errors should include "{text}"') def step_validation_errors_include(context: Context, text: str) -> None: assert any(text in e for e in context.validation_errors), ( f"Expected '{text}' in validation errors: {context.validation_errors}" ) @then('the node with label "{label}" should have parent "{parent_uri}"') def step_then_node_has_parent( context: Context, label: str, parent_uri: str, ) -> None: matching = [n for n in context.layer_nodes if n.label == label] assert matching, f"No node with label '{label}' found in layer nodes" node = matching[0] assert parent_uri in node.parent_uris, ( f"Expected parent '{parent_uri}' for node '{label}'; got: {node.parent_uris}" ) @then( 'the node with label "{label}" should have property "{prop}" resolved to a full URI' ) def step_then_property_resolved(context: Context, label: str, prop: str) -> None: matching = [n for n in context.layer_nodes if n.label == label] assert matching, f"No node with label '{label}' found in layer nodes" node = matching[0] prop_dict = dict(node.properties) assert prop in prop_dict, ( f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}" ) value = prop_dict[prop] assert value.startswith("http://") or value.startswith("https://"), ( f"Expected full URI for '{prop}' on '{label}', got: '{value}'" ) @then( 'the node with label "{label}" should have property "{prop}" equal to "{expected}"' ) def step_then_property_equal( context: Context, label: str, prop: str, expected: str, ) -> None: matching = [n for n in context.layer_nodes if n.label == label] assert matching, f"No node with label '{label}' found in layer nodes" node = matching[0] prop_dict = dict(node.properties) assert prop in prop_dict, ( f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}" ) assert prop_dict[prop] == expected, ( f"Expected '{prop}' == '{expected}' on '{label}', got: '{prop_dict[prop]}'" ) @then('the node with label "{label}" should not have property "{prop}"') def step_then_property_absent(context: Context, label: str, prop: str) -> None: matching = [n for n in context.layer_nodes if n.label == label] assert matching, f"No node with label '{label}' found in layer nodes" node = matching[0] prop_dict = dict(node.properties) assert prop not in prop_dict, ( f"Expected no '{prop}' on '{label}', but found: '{prop_dict[prop]}'" ) @then("a cycle detection error should be raised") def step_then_cycle_error(context: Context) -> None: assert context.cycle_error is not None, "Expected cycle error but none raised" assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error @then('a validation error should mention "{text}"') def step_then_validation_error_mention(context: Context, text: str) -> None: assert context.validation_error_msg is not None, ( "Expected a UKOValidationError but none was raised" ) assert text in context.validation_error_msg, ( f"Expected '{text}' in error message: {context.validation_error_msg}" ) # --------------------------------------------------------------------------- # Duplicate-key handling in properties tuple # --------------------------------------------------------------------------- @given("a UKO node with duplicate property keys") def step_given_duplicate_props(context: Context) -> None: context.dup_node = UKONode( uri="https://cleveragents.ai/ontology/uko#TestDup", rdf_type="owl:DatatypeProperty", label="TestDup", properties=( ("rdfs:domain", "https://cleveragents.ai/ontology/uko#First"), ("rdfs:domain", "https://cleveragents.ai/ontology/uko#Second"), ), ) @when("I convert the node properties to a dict") def step_when_convert_props_dict(context: Context) -> None: context.prop_dict = dict(context.dup_node.properties) @then("the dict should contain only the last value for the duplicated key") def step_then_last_value(context: Context) -> None: assert context.prop_dict["rdfs:domain"] == ( "https://cleveragents.ai/ontology/uko#Second" ), f"Expected last value 'Second', got: {context.prop_dict['rdfs:domain']}"