"""Step definitions for the ACMS Backend Abstraction Layer feature.""" from __future__ import annotations from dataclasses import FrozenInstanceError from typing import Any from behave import given, then, when from cleveragents.application.container import get_container, reset_container from cleveragents.domain.models.acms.backends import ( GraphBackend, GraphResult, TextBackend, TextResult, VectorBackend, VectorResult, ) from cleveragents.domain.models.acms.stubs import ( InMemoryGraphBackend, InMemoryTextBackend, InMemoryVectorBackend, ) __all__: list[str] = [] # --------------------------------------------------------------------------- # TextResult steps # --------------------------------------------------------------------------- @given('a TextResult with uko_uri "{uko_uri}" and score {score:g}') def step_given_text_result(context: Any, uko_uri: str, score: float) -> None: context.text_result = TextResult( uko_uri=uko_uri, content="class Auth: pass", score=score, ) @then('the text result uko_uri should be "{expected}"') def step_then_text_result_uko_uri(context: Any, expected: str) -> None: assert context.text_result.uko_uri == expected @then('the text result content should be "{expected}"') def step_then_text_result_content(context: Any, expected: str) -> None: assert context.text_result.content == expected @then("the text result score should be {expected:g}") def step_then_text_result_score(context: Any, expected: float) -> None: assert context.text_result.score == expected @then("the text result metadata should be empty") def step_then_text_result_metadata_empty(context: Any) -> None: assert context.text_result.metadata == {} @then("creating a TextResult with empty uko_uri should raise ValueError") def step_then_text_result_empty_uko_uri(context: Any) -> None: try: TextResult(uko_uri="", content="x", score=0.5) raise AssertionError("Expected ValueError") except ValueError: pass @then("creating a TextResult with score {score:g} should raise ValueError") def step_then_text_result_bad_score(context: Any, score: float) -> None: try: TextResult(uko_uri="uko:x", content="x", score=score) raise AssertionError("Expected ValueError") except ValueError: pass @then("modifying the text result score should raise an error") def step_then_text_result_immutable(context: Any) -> None: try: context.text_result.score = 0.1 # type: ignore[misc] raise AssertionError("Expected FrozenInstanceError") except FrozenInstanceError: pass # --------------------------------------------------------------------------- # VectorResult steps # --------------------------------------------------------------------------- @given('a VectorResult with uko_uri "{uko_uri}" and score {score:g}') def step_given_vector_result(context: Any, uko_uri: str, score: float) -> None: context.vector_result = VectorResult( uko_uri=uko_uri, content="vector store content", score=score, ) @then('the vector result uko_uri should be "{expected}"') def step_then_vector_result_uko_uri(context: Any, expected: str) -> None: assert context.vector_result.uko_uri == expected @then('the vector result content should be "{expected}"') def step_then_vector_result_content(context: Any, expected: str) -> None: assert context.vector_result.content == expected @then("the vector result score should be {expected:g}") def step_then_vector_result_score(context: Any, expected: float) -> None: assert context.vector_result.score == expected @then("the vector result metadata should be empty") def step_then_vector_result_metadata_empty(context: Any) -> None: assert context.vector_result.metadata == {} @then("creating a VectorResult with empty uko_uri should raise ValueError") def step_then_vector_result_empty_uko_uri(context: Any) -> None: try: VectorResult(uko_uri="", content="x", score=0.5) raise AssertionError("Expected ValueError") except ValueError: pass @then("creating a VectorResult with score {score:g} should raise ValueError") def step_then_vector_result_bad_score(context: Any, score: float) -> None: try: VectorResult(uko_uri="uko:x", content="x", score=score) raise AssertionError("Expected ValueError") except ValueError: pass @then("modifying the vector result score should raise an error") def step_then_vector_result_immutable(context: Any) -> None: try: context.vector_result.score = 0.1 # type: ignore[misc] raise AssertionError("Expected FrozenInstanceError") except FrozenInstanceError: pass # --------------------------------------------------------------------------- # GraphResult steps # --------------------------------------------------------------------------- @given("a GraphResult with no triples") def step_given_graph_result_empty(context: Any) -> None: context.graph_result = GraphResult() @given('a GraphResult with triple "{s}" "{p}" "{o}"') def step_given_graph_result_triple(context: Any, s: str, p: str, o: str) -> None: context.graph_result = GraphResult(triples=[(s, p, o)]) @then("the graph result triples should be empty") def step_then_graph_result_triples_empty(context: Any) -> None: assert context.graph_result.triples == [] @then("the graph result metadata should be empty") def step_then_graph_result_metadata_empty(context: Any) -> None: assert context.graph_result.metadata == {} @then("the graph result should have {count:d} triple") def step_then_graph_result_triple_count(context: Any, count: int) -> None: assert len(context.graph_result.triples) == count @then("modifying the graph result metadata should raise an error") def step_then_graph_result_immutable(context: Any) -> None: try: context.graph_result.metadata = {"x": "y"} # type: ignore[misc] raise AssertionError("Expected FrozenInstanceError") except FrozenInstanceError: pass # --------------------------------------------------------------------------- # InMemoryTextBackend steps # --------------------------------------------------------------------------- @given("an InMemoryTextBackend instance") def step_given_text_backend(context: Any) -> None: context.backend = InMemoryTextBackend() @then("it should be a TextBackend") def step_then_is_text_backend(context: Any) -> None: assert isinstance(context.backend, TextBackend) @when('I search for "{query}" with scope "{scope}"') def step_when_text_search(context: Any, query: str, scope: str) -> None: context.search_results = context.backend.search(query, scope=frozenset({scope})) @when('I search for "{query}" with scope "{scope}" and max_results {n:d}') def step_when_text_search_max(context: Any, query: str, scope: str, n: int) -> None: context.search_results = context.backend.search( query, scope=frozenset({scope}), max_results=n ) @then("the text search results should be empty") def step_then_text_search_empty(context: Any) -> None: assert context.search_results == [] @then("searching with empty query should raise ValueError") def step_then_text_search_empty_query(context: Any) -> None: try: context.backend.search("", scope=frozenset({"RES01"})) raise AssertionError("Expected ValueError") except ValueError: pass @then("searching with max_results {n:d} should raise ValueError") def step_then_text_search_bad_max(context: Any, n: int) -> None: try: context.backend.search("q", scope=frozenset({"RES01"}), max_results=n) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # InMemoryVectorBackend steps # --------------------------------------------------------------------------- @given("an InMemoryVectorBackend instance") def step_given_vector_backend(context: Any) -> None: context.backend = InMemoryVectorBackend() @then("it should be a VectorBackend") def step_then_is_vector_backend(context: Any) -> None: assert isinstance(context.backend, VectorBackend) @when('I similarity search with embedding and scope "{scope}"') def step_when_vector_search(context: Any, scope: str) -> None: context.search_results = context.backend.similarity_search( [0.1, 0.2, 0.3], scope=frozenset({scope}) ) @when('I similarity search with embedding and scope "{scope}" and top_k {k:d}') def step_when_vector_search_topk(context: Any, scope: str, k: int) -> None: context.search_results = context.backend.similarity_search( [0.1, 0.2, 0.3], scope=frozenset({scope}), top_k=k ) @then("the vector search results should be empty") def step_then_vector_search_empty(context: Any) -> None: assert context.search_results == [] @then("similarity searching with empty embedding should raise ValueError") def step_then_vector_search_empty_embed(context: Any) -> None: try: context.backend.similarity_search([], scope=frozenset({"RES01"})) raise AssertionError("Expected ValueError") except ValueError: pass @then("similarity searching with top_k {k:d} should raise ValueError") def step_then_vector_search_bad_topk(context: Any, k: int) -> None: try: context.backend.similarity_search([0.1], scope=frozenset({"RES01"}), top_k=k) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # InMemoryGraphBackend steps # --------------------------------------------------------------------------- @given("an InMemoryGraphBackend instance") def step_given_graph_backend(context: Any) -> None: context.backend = InMemoryGraphBackend() @then("it should be a GraphBackend") def step_then_is_graph_backend(context: Any) -> None: assert isinstance(context.backend, GraphBackend) @when('I run sparql query "{query}" with scope "{scope}"') def step_when_sparql_query(context: Any, query: str, scope: str) -> None: context.graph_query_result = context.backend.sparql_query( query, scope=frozenset({scope}) ) @then("the graph query result should have no triples") def step_then_graph_query_empty(context: Any) -> None: assert context.graph_query_result.triples == [] @then("sparql querying with empty query should raise ValueError") def step_then_sparql_empty_query(context: Any) -> None: try: context.backend.sparql_query("", scope=frozenset({"RES01"})) raise AssertionError("Expected ValueError") except ValueError: pass @when('I get triples for subject "{subject}"') def step_when_get_triples(context: Any, subject: str) -> None: context.graph_query_result = context.backend.get_triples(subject) @then("getting triples with empty subject should raise ValueError") def step_then_get_triples_empty_subject(context: Any) -> None: try: context.backend.get_triples("") raise AssertionError("Expected ValueError") except ValueError: pass @when('I traverse from "{start}" with depth {depth:d}') def step_when_traverse(context: Any, start: str, depth: int) -> None: context.graph_query_result = context.backend.traverse(start, depth=depth) @then("traversing with empty start should raise ValueError") def step_then_traverse_empty_start(context: Any) -> None: try: context.backend.traverse("") raise AssertionError("Expected ValueError") except ValueError: pass @then("traversing with depth {depth:d} should raise ValueError") def step_then_traverse_bad_depth(context: Any, depth: int) -> None: try: context.backend.traverse("uko:x", depth=depth) raise AssertionError("Expected ValueError") except ValueError: pass # --------------------------------------------------------------------------- # DI Container steps # --------------------------------------------------------------------------- @given("the DI container") def step_given_container(context: Any) -> None: reset_container() context.container = get_container() @then("the container should provide a text_backend") def step_then_container_text(context: Any) -> None: context.resolved_text = context.container.text_backend() assert context.resolved_text is not None @then("the text_backend should be a TextBackend") def step_then_text_is_protocol(context: Any) -> None: assert isinstance(context.resolved_text, TextBackend) @then("the container should provide a vector_backend") def step_then_container_vector(context: Any) -> None: context.resolved_vector = context.container.vector_backend() assert context.resolved_vector is not None @then("the vector_backend should be a VectorBackend") def step_then_vector_is_protocol(context: Any) -> None: assert isinstance(context.resolved_vector, VectorBackend) @then("the container should provide a graph_backend") def step_then_container_graph(context: Any) -> None: context.resolved_graph = context.container.graph_backend() assert context.resolved_graph is not None @then("the graph_backend should be a GraphBackend") def step_then_graph_is_protocol(context: Any) -> None: assert isinstance(context.resolved_graph, GraphBackend) @then("resolving text_backend twice should return the same instance") def step_then_text_singleton(context: Any) -> None: a = context.container.text_backend() b = context.container.text_backend() assert a is b @then("resolving vector_backend twice should return the same instance") def step_then_vector_singleton(context: Any) -> None: a = context.container.vector_backend() b = context.container.vector_backend() assert a is b @then("resolving graph_backend twice should return the same instance") def step_then_graph_singleton(context: Any) -> None: a = context.container.graph_backend() b = context.container.graph_backend() assert a is b