Files
placeholder/features/steps/temporal_data_model_model_steps.py
hamza.khyari d5f7f15215 feat(acms): implement Temporal Data Model (Revision-Aware RDF) with 3 storage tiers
Temporal metadata fields (validFrom, validUntil, isCurrent, isRevisionOf)
on UKO InformationUnit nodes enable revision chain tracking: when code
changes, old nodes are marked historical and new revision nodes are
created with back-links. Three storage tiers (hot/warm/cold) filter
nodes by temporal scope (current/recent/all) with configurable retention
(warm_retention_hours default 24h, cold_retention_days default 90d).

Includes TemporalMetadata, TemporalNode, RevisionChain, TierQueryResult,
TierRetentionConfig frozen domain models, TemporalBackend protocol,
InMemoryTemporalBackend stub, TemporalService with structlog and DI,
BackendSet.temporal typing upgrade from object|None to TemporalBackend|None.
67 Behave scenarios, 8 Robot Framework tests, ASV benchmarks, and
reference documentation.

ISSUES CLOSED: #577
2026-03-11 01:36:06 +00:00

432 lines
13 KiB
Python

"""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