"""Step definitions for Temporal Data Model — metadata and node model steps.""" from __future__ import annotations from datetime import UTC, datetime, timedelta, timezone from typing import Any from behave import given, then from pydantic import ValidationError from cleveragents.domain.models.acms.temporal import ( TemporalMetadata, TemporalNode, ) __all__: list[str] = [] # --------------------------------------------------------------------------- # TemporalMetadata steps # --------------------------------------------------------------------------- @given("a TemporalMetadata with valid_from now") def step_given_temporal_metadata_defaults(context: Any) -> None: context.temporal_meta = TemporalMetadata( valid_from=datetime.now(tz=UTC), ) @given( "a TemporalMetadata with valid_from now and valid_until tomorrow" ' and is_current false and is_revision_of "{revision_of}"' ) def step_given_temporal_metadata_all_fields( context: Any, revision_of: str, ) -> None: now = datetime.now(tz=UTC) context.temporal_meta = TemporalMetadata( valid_from=now, valid_until=now + timedelta(days=1), is_current=False, is_revision_of=revision_of, ) @given("a TemporalMetadata with naive valid_from") def step_given_temporal_metadata_naive_valid_from(context: Any) -> None: context.temporal_meta = TemporalMetadata( valid_from=datetime(2026, 1, 15, 10, 30, 0), ) @given("a TemporalMetadata with naive valid_until") def step_given_temporal_metadata_naive_valid_until(context: Any) -> None: context.temporal_meta = TemporalMetadata( valid_from=datetime.now(tz=UTC), valid_until=datetime(2026, 12, 31, 23, 59, 59), is_current=False, ) @then("the temporal metadata is_current should be true") def step_then_temporal_metadata_is_current_true(context: Any) -> None: assert context.temporal_meta.is_current is True @then("the temporal metadata is_current should be false") def step_then_temporal_metadata_is_current_false(context: Any) -> None: assert context.temporal_meta.is_current is False @then("the temporal metadata valid_until should be None") def step_then_temporal_metadata_valid_until_none(context: Any) -> None: assert context.temporal_meta.valid_until is None @then("the temporal metadata valid_until should not be None") def step_then_temporal_metadata_valid_until_not_none(context: Any) -> None: assert context.temporal_meta.valid_until is not None @then("the temporal metadata is_revision_of should be None") def step_then_temporal_metadata_revision_none(context: Any) -> None: assert context.temporal_meta.is_revision_of is None @then('the temporal metadata is_revision_of should be "{expected}"') def step_then_temporal_metadata_revision_of( context: Any, expected: str, ) -> None: assert context.temporal_meta.is_revision_of == expected @then("the temporal metadata valid_from should have UTC timezone") def step_then_temporal_metadata_valid_from_utc(context: Any) -> None: assert context.temporal_meta.valid_from.tzinfo is not None @then("the temporal metadata valid_until should have UTC timezone") def step_then_temporal_metadata_valid_until_utc(context: Any) -> None: assert context.temporal_meta.valid_until is not None assert context.temporal_meta.valid_until.tzinfo is not None @then("modifying the temporal metadata is_current should raise an error") def step_then_temporal_metadata_frozen(context: Any) -> None: try: context.temporal_meta.is_current = False # type: ignore[misc] msg = "Expected frozen model to reject mutation" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalMetadata with empty is_revision_of should raise ValueError") def step_then_temporal_metadata_empty_revision(context: Any) -> None: try: TemporalMetadata( valid_from=datetime.now(tz=UTC), is_revision_of="", ) msg = "Expected ValueError for empty is_revision_of" raise AssertionError(msg) except ValidationError: pass @then( "creating a TemporalMetadata with whitespace is_revision_of should raise ValueError" ) def step_then_temporal_metadata_whitespace_revision(context: Any) -> None: try: TemporalMetadata( valid_from=datetime.now(tz=UTC), is_revision_of=" ", ) msg = "Expected ValueError for whitespace is_revision_of" raise AssertionError(msg) except ValidationError: pass @given("a TemporalMetadata with non-UTC timezone") def step_given_temporal_metadata_non_utc(context: Any) -> None: est = timezone(timedelta(hours=-5)) context.temporal_meta = TemporalMetadata( valid_from=datetime(2026, 6, 15, 12, 0, tzinfo=est), is_current=True, ) @then( "creating a TemporalMetadata with valid_until before valid_from" " should raise ValueError", ) def step_then_temporal_valid_until_before_valid_from(context: Any) -> None: try: now = datetime.now(tz=UTC) TemporalMetadata( valid_from=now + timedelta(hours=1), valid_until=now, is_current=False, ) msg = "Expected ValueError for valid_until < valid_from" raise AssertionError(msg) except ValidationError: pass @then( "creating a TemporalMetadata with is_current true and valid_until set" " should raise ValueError", ) def step_then_temporal_is_current_with_valid_until(context: Any) -> None: try: now = datetime.now(tz=UTC) TemporalMetadata( valid_from=now, valid_until=now + timedelta(hours=1), is_current=True, ) msg = "Expected ValueError for is_current=True with valid_until" raise AssertionError(msg) except ValidationError: pass # --------------------------------------------------------------------------- # TemporalNode steps # --------------------------------------------------------------------------- @given( 'a TemporalNode with uri "{uri}" and resource "{resource}"' ' and path "{path}" without range' ) def step_given_temporal_node_no_range( context: Any, uri: str, resource: str, path: str, ) -> None: context.temporal_node = TemporalNode( node_uri=uri, source_resource=resource, source_path=path, temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) @given( 'a TemporalNode with uri "{uri}" and resource "{resource}"' ' and path "{path}" and range "{src_range}"' ) def step_given_temporal_node_with_range( context: Any, uri: str, resource: str, path: str, src_range: str, ) -> None: context.temporal_node = TemporalNode( node_uri=uri, source_resource=resource, source_path=path, source_range=src_range, temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) @then('the temporal node uri should be "{expected}"') def step_then_temporal_node_uri(context: Any, expected: str) -> None: assert context.temporal_node.node_uri == expected @then('the temporal node source_resource should be "{expected}"') def step_then_temporal_node_resource(context: Any, expected: str) -> None: assert context.temporal_node.source_resource == expected @then('the temporal node source_path should be "{expected}"') def step_then_temporal_node_path(context: Any, expected: str) -> None: assert context.temporal_node.source_path == expected @then("the temporal node source_range should be None") def step_then_temporal_node_range_none(context: Any) -> None: assert context.temporal_node.source_range is None @then('the temporal node source_range should be "{expected}"') def step_then_temporal_node_range(context: Any, expected: str) -> None: assert context.temporal_node.source_range == expected @then("the temporal node should have temporal metadata") def step_then_temporal_node_has_metadata(context: Any) -> None: assert isinstance(context.temporal_node.temporal, TemporalMetadata) @then("modifying the temporal node uri should raise an error") def step_then_temporal_node_frozen(context: Any) -> None: try: context.temporal_node.node_uri = "changed" # type: ignore[misc] msg = "Expected frozen model to reject mutation" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with empty node_uri should raise ValueError") def step_then_temporal_node_empty_uri(context: Any) -> None: try: TemporalNode( node_uri="", source_resource="RES", source_path="src/a.py", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with whitespace node_uri should raise ValueError") def step_then_temporal_node_whitespace_uri(context: Any) -> None: try: TemporalNode( node_uri=" ", source_resource="RES", source_path="src/a.py", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with empty source_resource should raise ValueError") def step_then_temporal_node_empty_resource(context: Any) -> None: try: TemporalNode( node_uri="uko:test", source_resource="", source_path="src/a.py", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with empty source_path should raise ValueError") def step_then_temporal_node_empty_path(context: Any) -> None: try: TemporalNode( node_uri="uko:test", source_resource="RES", source_path="", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with empty source_range should raise ValueError") def step_then_temporal_node_empty_range(context: Any) -> None: try: TemporalNode( node_uri="uko:test", source_resource="RES", source_path="src/a.py", source_range="", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass @then("creating a TemporalNode with whitespace source_range should raise ValueError") def step_then_temporal_node_whitespace_range(context: Any) -> None: try: TemporalNode( node_uri="uko:test", source_resource="RES", source_path="src/a.py", source_range=" ", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError" raise AssertionError(msg) except ValidationError: pass # --------------------------------------------------------------------------- # Round 4: string datetime coercion (NEW-3) # --------------------------------------------------------------------------- @given("a TemporalMetadata with valid_from as ISO-8601 string") def step_given_metadata_string_datetime(context: Any) -> None: context.temporal_meta = TemporalMetadata.model_validate( { "valid_from": "2025-06-15T12:00:00+03:00", "is_current": True, } ) # --------------------------------------------------------------------------- # Round 5: zero-width Unicode (NEW-14) + symmetric invariant (NEW-15) # --------------------------------------------------------------------------- @then( "creating a TemporalNode with zero-width Unicode node_uri should raise ValueError" ) def step_then_zero_width_uri_raises(context: Any) -> None: try: TemporalNode( node_uri="\u200b\u200c\u200d", source_resource="RES001", source_path="src/a.py", temporal=TemporalMetadata(valid_from=datetime.now(tz=UTC)), ) msg = "Expected ValueError for zero-width Unicode URI" raise AssertionError(msg) except ValidationError: pass @then( "creating a TemporalMetadata with is_current false" " and no valid_until should raise ValueError" ) def step_then_historical_without_valid_until_raises(context: Any) -> None: try: TemporalMetadata( valid_from=datetime.now(tz=UTC), is_current=False, ) msg = "Expected ValueError for is_current=False without valid_until" raise AssertionError(msg) except ValidationError: pass @then( "creating a TemporalMetadata with zero-width Unicode is_revision_of" " should raise ValueError" ) def step_then_zero_width_revision_of_raises(context: Any) -> None: try: TemporalMetadata( valid_from=datetime.now(tz=UTC), is_revision_of="\u200b\u200c", ) msg = "Expected ValueError for zero-width Unicode is_revision_of" raise AssertionError(msg) except ValidationError: pass