"""Step definitions for Temporal Data Model — backend steps.""" from __future__ import annotations from datetime import UTC, datetime, timedelta from typing import Any from behave import given, then, when from cleveragents.domain.models.acms.temporal import ( TemporalMetadata, TemporalNode, TierRetentionConfig, ) from cleveragents.domain.models.acms.temporal_stubs import InMemoryTemporalBackend from cleveragents.domain.models.acms.tiers import ContextTier from cleveragents.domain.models.core.project import TemporalScope __all__: list[str] = [] # ---- InMemoryTemporalBackend steps ---- @given("an InMemoryTemporalBackend instance") def step_given_inmemory_temporal_backend(context: Any) -> None: context.temporal_backend = InMemoryTemporalBackend() @given('a stored temporal node "{uri}" for resource "{resource}" at "{path}"') def step_given_stored_temporal_node( context: Any, uri: str, resource: str, path: str, ) -> None: now = datetime.now(tz=UTC) node = TemporalNode( node_uri=uri, source_resource=resource, source_path=path, temporal=TemporalMetadata( valid_from=now - timedelta(days=5), is_current=True, ), ) context.temporal_backend.store_node(node) context.last_stored_node = node @when('I get the current node for base "{base}"') def step_when_get_current(context: Any, base: str) -> None: context.current_node = context.temporal_backend.get_current(base) @then('the current node uri should be "{expected}"') def step_then_current_node_uri(context: Any, expected: str) -> None: assert context.current_node is not None assert context.current_node.node_uri == expected @then("the current node should be None") def step_then_current_node_none(context: Any) -> None: assert context.current_node is None @given('I create a revision from "{old}" to "{new}"') @when('I create a revision from "{old}" to "{new}"') def step_when_create_revision(context: Any, old: str, new: str) -> None: now = datetime.now(tz=UTC) new_node = TemporalNode( node_uri=new, source_resource="RES001", source_path="src/auth.py", temporal=TemporalMetadata( valid_from=now, is_current=True, is_revision_of=old, ), ) context.new_revision = context.temporal_backend.create_revision( old, new_node, now, ) @then('the old node "{uri}" should not be current') def step_then_old_node_not_current(context: Any, uri: str) -> None: node = context.temporal_backend._nodes[uri] assert node.temporal.is_current is False @then('the old node "{uri}" should have valid_until set') def step_then_old_node_valid_until(context: Any, uri: str) -> None: node = context.temporal_backend._nodes[uri] assert node.temporal.valid_until is not None @then('the new node "{uri}" should be current') def step_then_new_node_current(context: Any, uri: str) -> None: node = context.temporal_backend._nodes[uri] assert node.temporal.is_current is True @then('the new node "{uri}" should have is_revision_of "{expected}"') def step_then_new_node_revision_of( context: Any, uri: str, expected: str, ) -> None: node = context.temporal_backend._nodes[uri] assert node.temporal.is_revision_of == expected @then('creating a revision from nonexistent "{uri}" should raise KeyError') def step_then_revision_nonexistent(context: Any, uri: str) -> None: now = datetime.now(tz=UTC) new_node = TemporalNode( node_uri="uko:new", source_resource="RES", source_path="src/a.py", temporal=TemporalMetadata(valid_from=now, is_current=True), ) try: context.temporal_backend.create_revision(uri, new_node, now) msg = "Expected KeyError" raise AssertionError(msg) except KeyError: pass @then("creating a revision from blank URI should raise ValueError") def step_then_revision_blank_uri(context: Any) -> None: now = datetime.now(tz=UTC) new_node = TemporalNode( node_uri="uko:new", source_resource="RES", source_path="src/a.py", temporal=TemporalMetadata(valid_from=now, is_current=True), ) try: context.temporal_backend.create_revision("", new_node, now) msg = "Expected ValueError" raise AssertionError(msg) except ValueError: pass @then('creating a revision from "{uri}" to itself should raise ValueError') def step_then_revision_same_uri(context: Any, uri: str) -> None: now = datetime.now(tz=UTC) same_uri_node = TemporalNode( node_uri=uri, source_resource="RES", source_path="src/a.py", temporal=TemporalMetadata(valid_from=now, is_current=True), ) try: context.temporal_backend.create_revision(uri, same_uri_node, now) msg = "Expected ValueError for same-URI revision" raise AssertionError(msg) except ValueError: pass @then('storing a duplicate node "{uri}" should raise ValueError') def step_then_store_duplicate_raises(context: Any, uri: str) -> None: now = datetime.now(tz=UTC) node = TemporalNode( node_uri=uri, source_resource="RES_DUP", source_path="src/dup.py", temporal=TemporalMetadata(valid_from=now, is_current=True), ) try: context.temporal_backend.store_node(node) msg = "Expected ValueError for duplicate store_node" raise AssertionError(msg) except ValueError: pass # ---- get_history steps ---- @when('I get the history for base "{base}" with scope ALL') def step_when_get_history_all(context: Any, base: str) -> None: context.history = context.temporal_backend.get_history( base, TemporalScope.ALL, ) @when('I get the history for base "{base}" with scope CURRENT') def step_when_get_history_current(context: Any, base: str) -> None: context.history = context.temporal_backend.get_history( base, TemporalScope.CURRENT, ) @then("the history should have {count:d} nodes") def step_then_history_count(context: Any, count: int) -> None: assert len(context.history) == count @then("the history should be ordered newest first") def step_then_history_ordered(context: Any) -> None: for i in range(len(context.history) - 1): assert ( context.history[i].temporal.valid_from >= context.history[i + 1].temporal.valid_from ) @then("getting history for blank base URI should raise ValueError") def step_then_history_blank_uri(context: Any) -> None: try: context.temporal_backend.get_history("", TemporalScope.ALL) msg = "Expected ValueError" raise AssertionError(msg) except ValueError: pass # ---- revision chain steps ---- @when('I get the revision chain for "{uri}"') def step_when_get_chain(context: Any, uri: str) -> None: context.chain = context.temporal_backend.get_revision_chain(uri) @then("the revision chain should have depth {expected:d}") def step_then_chain_depth_backend(context: Any, expected: int) -> None: assert context.chain.depth == expected @then('the revision chain current should be "{expected}"') def step_then_chain_current(context: Any, expected: str) -> None: assert context.chain.current_uri == expected @then('the revision chain predecessors should include "{uri}"') def step_then_chain_preds_include(context: Any, uri: str) -> None: assert uri in context.chain.predecessors @then("getting revision chain for nonexistent URI should raise KeyError") def step_then_chain_nonexistent(context: Any) -> None: try: context.temporal_backend.get_revision_chain("uko:nonexistent") msg = "Expected KeyError" raise AssertionError(msg) except KeyError: pass # ---- query by tier steps ---- @when("I query the HOT tier") def step_when_query_hot(context: Any) -> None: retention = TierRetentionConfig() context.tier_query = context.temporal_backend.query_by_tier( ContextTier.HOT, TemporalScope.CURRENT, retention, ) @when("I query the COLD tier") def step_when_query_cold(context: Any) -> None: retention = TierRetentionConfig() context.tier_query = context.temporal_backend.query_by_tier( ContextTier.COLD, TemporalScope.ALL, retention, ) @then("the tier query should return {count:d} node") def step_then_tier_query_count_singular(context: Any, count: int) -> None: assert len(context.tier_query.nodes) == count @then("the tier query should return {count:d} nodes") def step_then_tier_query_count(context: Any, count: int) -> None: assert len(context.tier_query.nodes) == count @then("the tier query node should be current") def step_then_tier_query_node_current(context: Any) -> None: for node in context.tier_query.nodes: assert node.temporal.is_current is True # ---- mark historical steps ---- @given('I mark "{uri}" as historical') @when('I mark "{uri}" as historical') def step_when_mark_historical(context: Any, uri: str) -> None: now = datetime.now(tz=UTC) context.marked_node = context.temporal_backend.mark_historical( uri, now, ) @then("the marked node should not be current") def step_then_marked_not_current(context: Any) -> None: assert context.marked_node.temporal.is_current is False @then("the marked node should have valid_until set") def step_then_marked_valid_until(context: Any) -> None: assert context.marked_node.temporal.valid_until is not None @then("marking blank URI as historical should raise ValueError") def step_then_mark_blank_uri(context: Any) -> None: try: context.temporal_backend.mark_historical( "", datetime.now(tz=UTC), ) msg = "Expected ValueError" raise AssertionError(msg) except ValueError: pass @then("marking nonexistent URI as historical should raise KeyError") def step_then_mark_nonexistent(context: Any) -> None: try: context.temporal_backend.mark_historical( "uko:nonexistent", datetime.now(tz=UTC), ) msg = "Expected KeyError" raise AssertionError(msg) except KeyError: pass # ---- Edge-case guards (Round 3 bug-hunt fixes) ---- @then( 'marking already-historical "{uri}" should raise ValueError', ) def step_then_mark_already_historical(context: Any, uri: str) -> None: try: context.temporal_backend.mark_historical(uri, datetime.now(tz=UTC)) msg = "Expected ValueError for already-historical node" raise AssertionError(msg) except ValueError: pass @then( 'creating a revision from historical "{current}" ' 'to "{new_uri}" should raise ValueError', ) def step_then_revise_historical_raises( context: Any, current: str, new_uri: str, ) -> None: now = datetime.now(tz=UTC) new_node = TemporalNode( node_uri=new_uri, source_resource="RES001", source_path="src/auth.py", temporal=TemporalMetadata( valid_from=now, is_current=True, is_revision_of=current, ), ) try: context.temporal_backend.create_revision(current, new_node, now) msg = "Expected ValueError for revising historical node" raise AssertionError(msg) except ValueError: pass @then('the revision chain current_uri should be "{expected}"') def step_then_chain_current_uri(context: Any, expected: str) -> None: assert context.chain.current_uri == expected @then('the revision chain predecessors should contain "{expected}"') def step_then_chain_predecessors_contain(context: Any, expected: str) -> None: assert expected in context.chain.predecessors @given("a stored temporal node with branching revision chain") def step_given_branching_chain(context: Any) -> None: """Create A → B and A → C (sibling branches) via _nodes.""" now = datetime.now(tz=UTC) base = "uko-py:class/Branch" nodes = context.temporal_backend._nodes for suffix, meta_kw in [ ( "_A", { "valid_from": now - timedelta(hours=2), "valid_until": now - timedelta(hours=1), "is_current": False, }, ), ( "_B", { "valid_from": now - timedelta(hours=1), "is_current": True, "is_revision_of": f"{base}_A", }, ), ("_C", {"valid_from": now, "is_current": True, "is_revision_of": f"{base}_A"}), ]: uri = f"{base}{suffix}" nodes[uri] = TemporalNode( node_uri=uri, source_resource="RES001", source_path="src/a.py", temporal=TemporalMetadata(**meta_kw), ) @then('the revision chain should not contain "{uri}"') def step_then_chain_not_contain(context: Any, uri: str) -> None: assert uri not in context.chain.all_uris, ( f"{uri} should not be in chain {context.chain.all_uris}" ) # ---- Diff-coverage steps (RECENT scope, cycle guard, WARM tier) ---- @when('I get the history for base "{base}" with scope RECENT') def step_when_get_history_recent(context: Any, base: str) -> None: context.history = context.temporal_backend.get_history( base, TemporalScope.RECENT, ) @given("a stored temporal node with cyclic revision chain") def step_given_cyclic_revision_chain(context: Any) -> None: """Create two nodes whose is_revision_of pointers form a cycle.""" now = datetime.now(tz=UTC) for uri, meta_kw in [ ( "uko-py:class/Cycle_A", { "valid_from": now - timedelta(days=2), "valid_until": now - timedelta(days=1), "is_current": False, "is_revision_of": "uko-py:class/Cycle_B", }, ), ( "uko-py:class/Cycle_B", { "valid_from": now, "is_current": True, "is_revision_of": "uko-py:class/Cycle_A", }, ), ]: context.temporal_backend.store_node( TemporalNode( node_uri=uri, source_resource="RES001", source_path="src/cycle.py", temporal=TemporalMetadata(**meta_kw), ) ) @given("a stored temporal node with dangling predecessor") def step_given_dangling_predecessor(context: Any) -> None: context.temporal_backend.store_node( TemporalNode( node_uri="uko-py:class/Dangling_B", source_resource="RES001", source_path="src/dangling.py", temporal=TemporalMetadata( valid_from=datetime.now(tz=UTC), is_current=True, is_revision_of="uko-py:class/Dangling_A", ), ) )