"""Step definitions for uko_runtime.feature. All steps prefixed with ``uko`` to avoid AmbiguousStep collisions. """ from __future__ import annotations import tempfile from typing import Any from behave import given, then, when # type: ignore[import-untyped] from cleveragents.application.services.temporal_service import TemporalService from cleveragents.application.services.uko_inference import ( UKOInferenceEngine, ) from cleveragents.application.services.uko_persistence import ( InMemoryPersistenceBackend, JSONFilePersistenceBackend, UKOGraphPersistence, ) from cleveragents.application.services.uko_query_interface import ( ClassificationResult, UKOQueryInterface, _infer_layer_from_uri, ) from cleveragents.domain.models.acms.analyzers import AnalyzerRegistry, UKOTriple from cleveragents.domain.models.acms.index_stubs import ( InMemoryGraphIndexBackend, InMemoryTextIndexBackend, InMemoryVectorIndexBackend, ) from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer from cleveragents.domain.models.acms.temporal_stubs import InMemoryTemporalBackend from cleveragents.domain.models.core.project import TemporalScope from cleveragents.domain.models.core.resource import Resource from features.mocks.uko_indexer_mocks import InMemoryContentReader # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _DEFAULT_PROJECT = "local/test" def _make_resource( resource_id: str, location: str = "src/main.py", ) -> Resource: return Resource( resource_id=resource_id, resource_type_name="git-checkout", classification="physical", location=location, ) def _make_indexer(context: Any) -> None: """Build a UKOIndexer from context state.""" from cleveragents.application.services.uko_indexer import UKOIndexer context.uko_indexer = UKOIndexer( analyzer_registry=context.uko_registry, graph_backend=context.uko_graph_backend, text_backend=context.uko_text_backend, vector_backend=context.uko_vector_backend, content_reader=context.uko_content_reader, ) # --------------------------------------------------------------------------- # Background / setup steps # --------------------------------------------------------------------------- @given("uko a PythonAnalyzer is registered") def step_uko_python_analyzer(context: Any) -> None: context.uko_registry = AnalyzerRegistry() context.uko_registry.register(PythonAnalyzer()) context.uko_graph_backend = InMemoryGraphIndexBackend() context.uko_text_backend = InMemoryTextIndexBackend() context.uko_vector_backend = InMemoryVectorIndexBackend() context.uko_content_reader = InMemoryContentReader() @given("uko a UKOIndexer with all backends") def step_uko_indexer_all_backends(context: Any) -> None: _make_indexer(context) # --------------------------------------------------------------------------- # Indexing steps # --------------------------------------------------------------------------- @when('uko I index a Python resource "{resource_id}" with content "{content}"') def step_uko_index_python_resource( context: Any, resource_id: str, content: str ) -> None: content = content.replace("\\n", "\n") context.uko_content_reader.set_content(resource_id, content) resource = _make_resource(resource_id) context.uko_index_result = context.uko_indexer.index_resource( resource, project=_DEFAULT_PROJECT ) # --------------------------------------------------------------------------- # Assertion steps — index result # --------------------------------------------------------------------------- @then("uko the index result should have triple_count greater than 0") def step_uko_triple_count_gt_0(context: Any) -> None: assert context.uko_index_result.triple_count > 0, ( f"Expected triple_count > 0, got {context.uko_index_result.triple_count}" ) # --------------------------------------------------------------------------- # Assertion steps — graph backend content # --------------------------------------------------------------------------- def _get_all_triples( graph_backend: InMemoryGraphIndexBackend, project: str, ) -> list[tuple[str, str, str]]: """Return all (subject, predicate, object) triples for *project*.""" bindings = graph_backend.query(project, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }") return [(b.get("s", ""), b.get("p", ""), b.get("o", "")) for b in bindings] @then('uko the graph backend should contain a triple with predicate "{predicate}"') def step_uko_graph_has_predicate(context: Any, predicate: str) -> None: triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT) predicates = [t[1] for t in triples] assert predicate in predicates, ( f"Expected predicate '{predicate}' in graph, found: {sorted(set(predicates))}" ) @then('uko the graph backend should contain a triple with object "{obj}"') def step_uko_graph_has_object(context: Any, obj: str) -> None: triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT) objects = [t[2] for t in triples] assert obj in objects, ( f"Expected object '{obj}' in graph, found: {sorted(set(objects))[:10]}" ) @then('uko the graph has a predicate-object pair "{predicate}" "{obj}"') def step_uko_graph_has_predicate_and_object( context: Any, predicate: str, obj: str ) -> None: triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT) matching = [(s, p, o) for s, p, o in triples if p == predicate and o == obj] assert matching, ( f"Expected triple with predicate='{predicate}' and object='{obj}', " f"found predicates: {sorted(set(t[1] for t in triples))}" ) # --------------------------------------------------------------------------- # Inference engine steps # --------------------------------------------------------------------------- @given("uko an inference engine") def step_uko_inference_engine(context: Any) -> None: context.uko_inference_engine = UKOInferenceEngine() context.uko_inference_triples: list[UKOTriple] = [] @given('uko triples with two subjects of the same type "{type_uri}"') def step_uko_two_subjects_same_type(context: Any, type_uri: str) -> None: context.uko_inference_triples = [ UKOTriple( subject_uri="uko://code/class/foo/Foo", predicate="rdf:type", object_uri=type_uri, ), UKOTriple( subject_uri="uko://code/class/foo/Bar", predicate="rdf:type", object_uri=type_uri, ), ] @given('uko triples with a parent subject "{parent}" and child "{child}"') def step_uko_parent_child_subjects(context: Any, parent: str, child: str) -> None: context.uko_inference_triples = [ UKOTriple( subject_uri=parent, predicate="rdf:type", object_uri="uko-code:Module", ), UKOTriple( subject_uri=child, predicate="rdf:type", object_uri="uko-code:Module", ), ] @given('uko triples where subject "{subject}" references "{ref}"') def step_uko_subject_references(context: Any, subject: str, ref: str) -> None: context.uko_inference_triples = [ UKOTriple( subject_uri=subject, predicate="rdf:type", object_uri="uko-code:Module", ), UKOTriple( subject_uri=ref, predicate="rdf:type", object_uri="uko-code:Module", ), UKOTriple( subject_uri=subject, predicate="uko:references", object_uri=ref, ), ] @when("uko I run inference") def step_uko_run_inference(context: Any) -> None: context.uko_inferred = context.uko_inference_engine.infer( context.uko_inference_triples ) @when("uko I run inference on empty triples") def step_uko_run_inference_empty(context: Any) -> None: context.uko_inferred = context.uko_inference_engine.infer([]) @then('uko the inferred triples should contain a "{predicate}" relationship') def step_uko_inferred_has_predicate(context: Any, predicate: str) -> None: predicates = [t.predicate for t in context.uko_inferred] assert predicate in predicates, ( f"Expected predicate '{predicate}' in inferred triples, found: {predicates}" ) @then("uko the inferred triples should be empty") def step_uko_inferred_empty(context: Any) -> None: assert context.uko_inferred == [], ( f"Expected empty inferred triples, got: {context.uko_inferred}" ) # --------------------------------------------------------------------------- # UKO Query Interface steps # --------------------------------------------------------------------------- @given('uko a graph backend with triples for resource "{resource_id}"') def step_uko_graph_with_resource(context: Any, resource_id: str) -> None: context.uko_graph_backend = InMemoryGraphIndexBackend() context.uko_query_resource_id = resource_id resource_uri = f"uko://resource/{resource_id}" # Add a sourceResource triple so the query interface can find it. context.uko_graph_backend.add_triple( _DEFAULT_PROJECT, f"uko://code/module/{resource_id}", "uko:sourceResource", resource_uri, ) @given("uko a graph backend with no triples") def step_uko_graph_no_triples(context: Any) -> None: context.uko_graph_backend = InMemoryGraphIndexBackend() context.uko_query_resource_id = "01HQ8ZDRX50000000000000099" @given('uko the graph has a type triple "{type_uri}" for the resource') def step_uko_graph_type_triple(context: Any, type_uri: str) -> None: resource_id = context.uko_query_resource_id resource_uri = f"uko://resource/{resource_id}" subject = f"uko://code/module/{resource_id}" context.uko_graph_backend.add_triple( _DEFAULT_PROJECT, subject, "rdf:type", type_uri, ) # Also add sourceResource link for the type subject. context.uko_graph_backend.add_triple( _DEFAULT_PROJECT, subject, "uko:sourceResource", resource_uri, ) @when('uko I classify resource "{resource_id}"') def step_uko_classify_resource(context: Any, resource_id: str) -> None: interface = UKOQueryInterface( graph_backend=context.uko_graph_backend, project=_DEFAULT_PROJECT, ) context.uko_classification = interface.classify_resource(resource_id) @then("uko the classification result should have layer {layer:d}") def step_uko_classification_layer(context: Any, layer: int) -> None: assert context.uko_classification.layer == layer, ( f"Expected layer {layer}, got {context.uko_classification.layer}" ) @then('uko the classification result should have primary_type "{type_uri}"') def step_uko_classification_primary_type(context: Any, type_uri: str) -> None: assert context.uko_classification.primary_type == type_uri, ( f"Expected primary_type '{type_uri}', " f"got '{context.uko_classification.primary_type}'" ) @then("uko classifying with empty resource_id should raise ValueError") def step_uko_classify_empty_id(context: Any) -> None: interface = UKOQueryInterface( graph_backend=context.uko_graph_backend, project=_DEFAULT_PROJECT, ) try: interface.classify_resource("") raise AssertionError("Expected ValueError") except ValueError: pass @then("uko creating UKOQueryInterface with invalid backend should raise TypeError") def step_uko_query_interface_invalid_backend(context: Any) -> None: try: UKOQueryInterface(graph_backend="not-a-backend", project=_DEFAULT_PROJECT) # type: ignore[arg-type] raise AssertionError("Expected TypeError") except TypeError: pass @then("uko creating UKOQueryInterface with empty project should raise ValueError") def step_uko_query_interface_empty_project(context: Any) -> None: backend = InMemoryGraphIndexBackend() try: UKOQueryInterface(graph_backend=backend, project="") raise AssertionError("Expected ValueError") except ValueError: pass @then("uko get_resources_by_layer with layer 4 should raise ValueError") def step_uko_get_resources_invalid_layer(context: Any) -> None: interface = UKOQueryInterface( graph_backend=context.uko_graph_backend, project=_DEFAULT_PROJECT, ) try: interface.get_resources_by_layer(4) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # UKO Graph Persistence steps # --------------------------------------------------------------------------- @when("uko I save the graph state") def step_uko_save_graph(context: Any) -> None: persistence_backend = InMemoryPersistenceBackend() context.uko_persistence_backend = persistence_backend service = UKOGraphPersistence( graph_backend=context.uko_graph_backend, project=_DEFAULT_PROJECT, persistence_backend=persistence_backend, ) context.uko_save_result = service.save() @when("uko I restore the graph state into a fresh backend") def step_uko_restore_graph(context: Any) -> None: fresh_backend = InMemoryGraphIndexBackend() context.uko_restored_backend = fresh_backend # Use the persistence backend from context if available, otherwise create a fresh one. persistence_backend = getattr(context, "uko_persistence_backend", None) if persistence_backend is None: persistence_backend = InMemoryPersistenceBackend() context.uko_persistence_backend = persistence_backend service = UKOGraphPersistence( graph_backend=fresh_backend, project=_DEFAULT_PROJECT, persistence_backend=persistence_backend, ) context.uko_restore_result = service.restore() @then("uko the restored backend should have triples") def step_uko_restored_has_triples(context: Any) -> None: bindings = context.uko_restored_backend.query( _DEFAULT_PROJECT, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }" ) assert len(bindings) > 0, "Expected restored backend to have triples" @then("uko the save result should be 0") def step_uko_save_result_zero(context: Any) -> None: assert context.uko_save_result == 0, ( f"Expected save result 0, got {context.uko_save_result}" ) @then("uko the restore result should be 0") def step_uko_restore_result_zero(context: Any) -> None: assert context.uko_restore_result == 0, ( f"Expected restore result 0, got {context.uko_restore_result}" ) @then("uko creating UKOGraphPersistence with empty project should raise ValueError") def step_uko_persistence_empty_project(context: Any) -> None: backend = InMemoryGraphIndexBackend() try: UKOGraphPersistence(graph_backend=backend, project="") raise AssertionError("Expected ValueError") except ValueError: pass @then("uko creating UKOGraphPersistence with invalid backend should raise TypeError") def step_uko_persistence_invalid_backend(context: Any) -> None: try: UKOGraphPersistence(graph_backend="not-a-backend", project=_DEFAULT_PROJECT) # type: ignore[arg-type] raise AssertionError("Expected TypeError") except TypeError: pass # --------------------------------------------------------------------------- # InMemoryPersistenceBackend steps # --------------------------------------------------------------------------- @given("uko an in-memory persistence backend") def step_uko_in_memory_persistence(context: Any) -> None: context.uko_persistence_backend = InMemoryPersistenceBackend() context.uko_test_triples = [ {"subject": "uko://s1", "predicate": "rdf:type", "object": "uko-py:Class"}, {"subject": "uko://s2", "predicate": "uko:contains", "object": "uko://s1"}, ] @when('uko I save triples to the persistence backend for project "{project}"') def step_uko_save_to_persistence(context: Any, project: str) -> None: context.uko_persistence_backend.save(project, context.uko_test_triples) context.uko_saved_project = project @then("uko loading from the persistence backend should return the same triples") def step_uko_load_from_persistence(context: Any) -> None: loaded = context.uko_persistence_backend.load(context.uko_saved_project) assert loaded == context.uko_test_triples, ( f"Expected {context.uko_test_triples}, got {loaded}" ) @then( "uko loading from the persistence backend for unknown project should return empty list" ) def step_uko_load_unknown_project(context: Any) -> None: loaded = context.uko_persistence_backend.load("unknown/project") assert loaded == [], f"Expected empty list, got {loaded}" @then("uko saving to persistence backend with empty project should raise ValueError") def step_uko_save_empty_project(context: Any) -> None: try: context.uko_persistence_backend.save("", []) raise AssertionError("Expected ValueError") except ValueError: pass @then("uko loading from persistence backend with empty project should raise ValueError") def step_uko_load_empty_project(context: Any) -> None: try: context.uko_persistence_backend.load("") raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # JSONFilePersistenceBackend steps # --------------------------------------------------------------------------- @given("uko a JSON file persistence backend in a temp directory") def step_uko_json_file_backend(context: Any) -> None: context.uko_temp_dir = tempfile.mkdtemp() context.uko_json_backend = JSONFilePersistenceBackend(base_dir=context.uko_temp_dir) context.uko_test_triples = [ {"subject": "uko://s1", "predicate": "rdf:type", "object": "uko-py:Class"}, ] @when('uko I save triples to the JSON file backend for project "{project}"') def step_uko_save_to_json_backend(context: Any, project: str) -> None: context.uko_json_backend.save(project, context.uko_test_triples) context.uko_saved_project = project @then("uko loading from the JSON file backend should return the same triples") def step_uko_load_from_json_backend(context: Any) -> None: loaded = context.uko_json_backend.load(context.uko_saved_project) assert loaded == context.uko_test_triples, ( f"Expected {context.uko_test_triples}, got {loaded}" ) @then( "uko loading from the JSON file backend for unknown project should return empty list" ) def step_uko_load_json_unknown_project(context: Any) -> None: loaded = context.uko_json_backend.load("unknown/project") assert loaded == [], f"Expected empty list, got {loaded}" @then("uko saving to JSON file backend with empty project should raise ValueError") def step_uko_save_json_empty_project(context: Any) -> None: try: context.uko_json_backend.save("", []) raise AssertionError("Expected ValueError") except ValueError: pass @then("uko loading from JSON file backend with empty project should raise ValueError") def step_uko_load_json_empty_project(context: Any) -> None: try: context.uko_json_backend.load("") raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # Temporal versioning steps # --------------------------------------------------------------------------- @given("uko a temporal backend") def step_uko_temporal_backend(context: Any) -> None: context.uko_temporal_backend = InMemoryTemporalBackend() context.uko_temporal_service = TemporalService(backend=context.uko_temporal_backend) @given('uko I store an initial node "{node_uri}" for resource "{resource_id}"') def step_uko_store_initial_node(context: Any, node_uri: str, resource_id: str) -> None: context.uko_temporal_service.store_initial_node( node_uri=node_uri, source_resource=resource_id, source_path="src/main.py", ) @given('uko I create a revision from "{current_uri}" to "{new_uri}"') @when('uko I create a revision from "{current_uri}" to "{new_uri}"') def step_uko_create_revision(context: Any, current_uri: str, new_uri: str) -> None: context.uko_temporal_service.create_revision( current_uri=current_uri, new_node_uri=new_uri, source_resource="RES001", source_path="src/main.py", ) @when('uko I mark "{node_uri}" as historical') def step_uko_mark_historical(context: Any, node_uri: str) -> None: context.uko_temporal_service.mark_historical(node_uri) @when('uko I get the revision chain for "{node_uri}"') def step_uko_get_revision_chain(context: Any, node_uri: str) -> None: context.uko_revision_chain = context.uko_temporal_service.get_revision_chain( node_uri ) @when('uko I get history for "{node_uri_base}" with scope ALL') def step_uko_get_history_all(context: Any, node_uri_base: str) -> None: context.uko_history = context.uko_temporal_service.get_history( node_uri_base, temporal_scope=TemporalScope.ALL ) @when('uko I get current for "{node_uri_base}"') def step_uko_get_current(context: Any, node_uri_base: str) -> None: context.uko_current_node = context.uko_temporal_service.get_current(node_uri_base) @then('uko the temporal backend should have node "{node_uri}"') def step_uko_temporal_has_node(context: Any, node_uri: str) -> None: node = context.uko_temporal_backend.get_current(node_uri) if node is None: # Check if it's historical. history = context.uko_temporal_backend.get_history(node_uri, TemporalScope.ALL) assert any(n.node_uri == node_uri for n in history), ( f"Expected node '{node_uri}' in temporal backend" ) @then("uko the node should be current") def step_uko_node_is_current(context: Any) -> None: # The last stored/created node should be current. pass # Verified by other steps @then('uko node "{node_uri}" should be historical') def step_uko_node_is_historical(context: Any, node_uri: str) -> None: history = context.uko_temporal_backend.get_history(node_uri, TemporalScope.ALL) historical = [ n for n in history if n.node_uri == node_uri and not n.temporal.is_current ] assert historical, f"Expected node '{node_uri}' to be historical" @then('uko node "{node_uri}" should be current') def step_uko_node_is_current_named(context: Any, node_uri: str) -> None: node = context.uko_temporal_backend.get_current(node_uri) assert node is not None and node.temporal.is_current, ( f"Expected node '{node_uri}' to be current" ) @then("uko the revision chain should have depth {depth:d}") def step_uko_chain_depth(context: Any, depth: int) -> None: assert context.uko_revision_chain.depth == depth, ( f"Expected chain depth {depth}, got {context.uko_revision_chain.depth}" ) @then('uko the revision chain current_uri should be "{current_uri}"') def step_uko_chain_current_uri(context: Any, current_uri: str) -> None: assert context.uko_revision_chain.current_uri == current_uri, ( f"Expected current_uri '{current_uri}', " f"got '{context.uko_revision_chain.current_uri}'" ) @then('uko the revision chain predecessors should contain "{predecessor_uri}"') def step_uko_chain_has_predecessor(context: Any, predecessor_uri: str) -> None: assert predecessor_uri in context.uko_revision_chain.predecessors, ( f"Expected '{predecessor_uri}' in predecessors " f"{context.uko_revision_chain.predecessors}" ) @then("uko the history should have {count:d} nodes") def step_uko_history_count(context: Any, count: int) -> None: assert len(context.uko_history) == count, ( f"Expected {count} history nodes, got {len(context.uko_history)}" ) @then('uko the current node should be "{node_uri}"') def step_uko_current_node_uri(context: Any, node_uri: str) -> None: assert context.uko_current_node is not None, "Expected current node, got None" assert context.uko_current_node.node_uri == node_uri, ( f"Expected current node '{node_uri}', got '{context.uko_current_node.node_uri}'" ) # --------------------------------------------------------------------------- # Layer inference helper steps # --------------------------------------------------------------------------- @then('uko inferring layer for "{uri}" should return {layer:d}') def step_uko_infer_layer(context: Any, uri: str, layer: int) -> None: result = _infer_layer_from_uri(uri) assert result == layer, f"Expected layer {layer} for URI '{uri}', got {result}" # --------------------------------------------------------------------------- # ClassificationResult validation steps # --------------------------------------------------------------------------- @then( "uko creating ClassificationResult with empty resource_id should raise ValueError" ) def step_uko_classification_empty_id(context: Any) -> None: try: ClassificationResult(resource_id="") raise AssertionError("Expected ValueError") except ValueError: pass @then("uko creating ClassificationResult with layer {layer:d} should raise ValueError") def step_uko_classification_invalid_layer(context: Any, layer: int) -> None: try: ClassificationResult(resource_id="RES001", layer=layer) raise AssertionError("Expected ValueError") except ValueError: pass @then("uko creating ClassificationResult with layer {layer:d} should succeed") def step_uko_classification_valid_layer(context: Any, layer: int) -> None: result = ClassificationResult(resource_id="RES001", layer=layer) assert result.layer == layer