Files
temp/features/steps/temporal_data_model_service_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

322 lines
10 KiB
Python

"""Step definitions for Temporal Data Model — service steps."""
from __future__ import annotations
from typing import Any
from behave import given, then, when
from cleveragents.application.services.temporal_service import TemporalService
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] = []
# ---------------------------------------------------------------------------
# TemporalService steps
# ---------------------------------------------------------------------------
@given("a TemporalService with InMemoryTemporalBackend")
def step_given_temporal_service(context: Any) -> None:
backend = InMemoryTemporalBackend()
context.temporal_service = TemporalService(backend=backend)
context.temporal_backend = backend
@given('I store an initial node "{uri}" for resource "{resource}" at "{path}"')
@when('I store an initial node "{uri}" for resource "{resource}" at "{path}"')
def step_when_store_initial(
context: Any,
uri: str,
resource: str,
path: str,
) -> None:
context.service_node = context.temporal_service.store_initial_node(
node_uri=uri,
source_resource=resource,
source_path=path,
)
@then("the stored node should be current")
def step_then_stored_current(context: Any) -> None:
assert context.service_node.temporal.is_current is True
@then("the stored node should have no predecessor")
def step_then_stored_no_predecessor(context: Any) -> None:
assert context.service_node.temporal.is_revision_of is None
@when(
'I create a service revision from "{old}" to "{new}"'
' for resource "{resource}" at "{path}"'
)
def step_when_service_create_revision(
context: Any,
old: str,
new: str,
resource: str,
path: str,
) -> None:
context.service_revision = context.temporal_service.create_revision(
current_uri=old,
new_node_uri=new,
source_resource=resource,
source_path=path,
)
@then("the service revision node should be current")
def step_then_service_revision_current(context: Any) -> None:
assert context.service_revision.temporal.is_current is True
@then('the service revision node should have is_revision_of "{expected}"')
def step_then_service_revision_of(context: Any, expected: str) -> None:
assert context.service_revision.temporal.is_revision_of == expected
@when('I get the service current for "{base}"')
def step_when_service_get_current(context: Any, base: str) -> None:
context.service_current = context.temporal_service.get_current(base)
@then('the service current node uri should be "{expected}"')
def step_then_service_current_uri(context: Any, expected: str) -> None:
assert context.service_current is not None
assert context.service_current.node_uri == expected
@when("I query HOT tier via the service")
def step_when_service_query_hot(context: Any) -> None:
context.service_tier_query = context.temporal_service.query_by_tier(
ContextTier.HOT,
)
@when("I query HOT tier via the service with scope ALL")
def step_when_service_query_hot_all(context: Any) -> None:
context.service_tier_query = context.temporal_service.query_by_tier(
ContextTier.HOT,
temporal_scope=TemporalScope.ALL,
)
@then("the service tier query scope should be CURRENT")
def step_then_service_tier_scope_current(context: Any) -> None:
assert context.service_tier_query.temporal_scope == TemporalScope.CURRENT
@then("the service tier query scope should be ALL")
def step_then_service_tier_scope_all(context: Any) -> None:
assert context.service_tier_query.temporal_scope == TemporalScope.ALL
@when('I mark "{uri}" as historical via the service')
def step_when_service_mark_historical(context: Any, uri: str) -> None:
context.service_marked = context.temporal_service.mark_historical(uri)
@then("the service marked node should not be current")
def step_then_service_marked_not_current(context: Any) -> None:
assert context.service_marked.temporal.is_current is False
@then("storing an initial node with blank URI via the service should raise ValueError")
def step_then_service_blank_uri(context: Any) -> None:
try:
context.temporal_service.store_initial_node(
node_uri="",
source_resource="RES",
source_path="src/a.py",
)
msg = "Expected ValueError"
raise AssertionError(msg)
except ValueError:
pass
@then(
"storing an initial node with blank source_resource via the service"
" should raise ValueError"
)
def step_then_service_blank_resource(context: Any) -> None:
try:
context.temporal_service.store_initial_node(
node_uri="uko:test",
source_resource="",
source_path="src/a.py",
)
msg = "Expected ValueError"
raise AssertionError(msg)
except ValueError:
pass
@when('I get the service history for "{base}" with scope ALL')
def step_when_service_get_history_all(context: Any, base: str) -> None:
context.service_history = context.temporal_service.get_history(
base,
temporal_scope=TemporalScope.ALL,
)
@when('I get the service history for "{base}" with scope CURRENT')
def step_when_service_get_history_current(context: Any, base: str) -> None:
context.service_history = context.temporal_service.get_history(
base,
temporal_scope=TemporalScope.CURRENT,
)
@then("the service history should have {count:d} nodes")
def step_then_service_history_count(context: Any, count: int) -> None:
assert len(context.service_history) == count, (
f"Expected {count} nodes, got {len(context.service_history)}"
)
@then("getting service history for blank base URI should raise ValueError")
def step_then_service_history_blank_raises(context: Any) -> None:
try:
context.temporal_service.get_history("")
msg = "Expected ValueError"
raise AssertionError(msg)
except ValueError:
pass
@when('I get the service revision chain for "{uri}"')
def step_when_service_get_chain(context: Any, uri: str) -> None:
context.service_chain = context.temporal_service.get_revision_chain(uri)
@then("the service revision chain depth should be {expected:d}")
def step_then_service_chain_depth(context: Any, expected: int) -> None:
assert context.service_chain.depth == expected, (
f"Expected depth {expected}, got {context.service_chain.depth}"
)
@then('the service revision chain current should be "{expected}"')
def step_then_service_chain_current(context: Any, expected: str) -> None:
assert context.service_chain.current_uri == expected, (
f"Expected {expected}, got {context.service_chain.current_uri}"
)
@then("getting service revision chain for blank URI should raise ValueError")
def step_then_service_chain_blank_raises(context: Any) -> None:
try:
context.temporal_service.get_revision_chain("")
msg = "Expected ValueError"
raise AssertionError(msg)
except ValueError:
pass
@when("I query COLD tier via the service with scope CURRENT")
def step_when_service_query_cold_current(context: Any) -> None:
context.service_tier_query = context.temporal_service.query_by_tier(
ContextTier.COLD,
temporal_scope=TemporalScope.CURRENT,
)
@then("the service tier query should return {count:d} node")
def step_then_service_tier_query_count(context: Any, count: int) -> None:
assert len(context.service_tier_query.nodes) == count, (
f"Expected {count} nodes, got {len(context.service_tier_query.nodes)}"
)
@then("the service tier query node should be current")
def step_then_service_tier_query_node_current(context: Any) -> None:
for node in context.service_tier_query.nodes:
assert node.temporal.is_current is True, f"Node {node.node_uri} is not current"
@then("the service retention warm hours should be {expected:d}")
def step_then_service_retention_warm(context: Any, expected: int) -> None:
assert context.temporal_service.retention.warm_retention_hours == expected
@then("the service retention cold days should be {expected:d}")
def step_then_service_retention_cold(context: Any, expected: int) -> None:
assert context.temporal_service.retention.cold_retention_days == expected
# ---------------------------------------------------------------------------
# Diff-coverage steps (WARM/COLD default scopes)
# ---------------------------------------------------------------------------
@when("I query WARM tier via the service")
def step_when_service_query_warm(context: Any) -> None:
context.service_tier_query = context.temporal_service.query_by_tier(
ContextTier.WARM,
)
@when("I query COLD tier via the service")
def step_when_service_query_cold(context: Any) -> None:
context.service_tier_query = context.temporal_service.query_by_tier(
ContextTier.COLD,
)
@then("the service tier query scope should be RECENT")
def step_then_service_tier_scope_recent(context: Any) -> None:
assert context.service_tier_query.temporal_scope == TemporalScope.RECENT
# ---------------------------------------------------------------------------
# Edge-case guards (Round 3 bug-hunt fixes)
# ---------------------------------------------------------------------------
@then(
'creating a service revision from "{uri}" to itself should raise ValueError',
)
def step_then_service_same_uri_raises(context: Any, uri: str) -> None:
try:
context.temporal_service.create_revision(
current_uri=uri,
new_node_uri=uri,
source_resource="RES001",
source_path="src/foo.py",
)
msg = "Expected ValueError for same-URI service revision"
raise AssertionError(msg)
except ValueError:
pass
@then(
'marking "{uri}" as historical again via the service should raise ValueError',
)
def step_then_service_mark_historical_again_raises(
context: Any,
uri: str,
) -> None:
try:
context.temporal_service.mark_historical(uri)
msg = "Expected ValueError for already-historical node"
raise AssertionError(msg)
except ValueError:
pass
@then("creating a TemporalService with None backend should raise TypeError")
def step_then_service_none_backend(context: Any) -> None:
try:
TemporalService(backend=None) # type: ignore[arg-type]
msg = "Expected TypeError for None backend"
raise AssertionError(msg)
except TypeError:
pass