d5f7f15215
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 20s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 2m50s
CI / integration_tests (pull_request) Successful in 3m22s
CI / docker (pull_request) Successful in 40s
CI / coverage (pull_request) Successful in 5m49s
CI / benchmark-regression (pull_request) Successful in 35m15s
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
497 lines
27 KiB
Gherkin
497 lines
27 KiB
Gherkin
Feature: Temporal Data Model (Revision-Aware RDF) with 3 Storage Tiers
|
|
As a developer
|
|
I want temporal metadata on UKO nodes with revision chains and tier-aware queries
|
|
So that the ACMS can track knowledge graph evolution and enable temporal-archaeology
|
|
|
|
# ---- TemporalMetadata model ----
|
|
Scenario: Create a valid TemporalMetadata with defaults
|
|
Given a TemporalMetadata with valid_from now
|
|
Then the temporal metadata is_current should be true
|
|
And the temporal metadata valid_until should be None
|
|
And the temporal metadata is_revision_of should be None
|
|
And the temporal metadata valid_from should have UTC timezone
|
|
|
|
Scenario: Create a TemporalMetadata with all fields
|
|
Given a TemporalMetadata with valid_from now and valid_until tomorrow and is_current false and is_revision_of "uko-py:class/Foo_v1"
|
|
Then the temporal metadata is_current should be false
|
|
And the temporal metadata valid_until should not be None
|
|
And the temporal metadata is_revision_of should be "uko-py:class/Foo_v1"
|
|
|
|
Scenario: TemporalMetadata is frozen
|
|
Given a TemporalMetadata with valid_from now
|
|
Then modifying the temporal metadata is_current should raise an error
|
|
|
|
Scenario: TemporalMetadata rejects empty is_revision_of
|
|
Then creating a TemporalMetadata with empty is_revision_of should raise ValueError
|
|
Scenario: TemporalMetadata rejects whitespace-only is_revision_of
|
|
Then creating a TemporalMetadata with whitespace is_revision_of should raise ValueError
|
|
Scenario: TemporalNode rejects zero-width Unicode node_uri
|
|
Then creating a TemporalNode with zero-width Unicode node_uri should raise ValueError
|
|
Scenario: TemporalMetadata rejects zero-width Unicode is_revision_of
|
|
Then creating a TemporalMetadata with zero-width Unicode is_revision_of should raise ValueError
|
|
|
|
Scenario: TemporalMetadata adds UTC to naive valid_from
|
|
Given a TemporalMetadata with naive valid_from
|
|
Then the temporal metadata valid_from should have UTC timezone
|
|
|
|
Scenario: TemporalMetadata adds UTC to naive valid_until
|
|
Given a TemporalMetadata with naive valid_until
|
|
Then the temporal metadata valid_until should have UTC timezone
|
|
|
|
Scenario: TemporalMetadata converts non-UTC aware datetime to UTC
|
|
Given a TemporalMetadata with non-UTC timezone
|
|
Then the temporal metadata valid_from should have UTC timezone
|
|
|
|
Scenario: TemporalMetadata coerces ISO-8601 string datetime to UTC
|
|
Given a TemporalMetadata with valid_from as ISO-8601 string
|
|
Then the temporal metadata valid_from should have UTC timezone
|
|
|
|
Scenario: TemporalMetadata rejects valid_until before valid_from
|
|
Then creating a TemporalMetadata with valid_until before valid_from should raise ValueError
|
|
Scenario: TemporalMetadata rejects is_current with valid_until set
|
|
Then creating a TemporalMetadata with is_current true and valid_until set should raise ValueError
|
|
Scenario: TemporalMetadata rejects is_current false without valid_until
|
|
Then creating a TemporalMetadata with is_current false and no valid_until should raise ValueError
|
|
|
|
# ---- TemporalNode model ----
|
|
Scenario: Create a valid TemporalNode
|
|
Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" without range
|
|
Then the temporal node uri should be "uko-py:class/Auth_v1"
|
|
And the temporal node source_resource should be "RES001"
|
|
And the temporal node source_path should be "src/auth.py"
|
|
And the temporal node source_range should be None
|
|
And the temporal node should have temporal metadata
|
|
|
|
Scenario: TemporalNode with source_range
|
|
Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" and range "15:1-87:0"
|
|
Then the temporal node source_range should be "15:1-87:0"
|
|
|
|
Scenario: TemporalNode is frozen
|
|
Given a TemporalNode with uri "uko-py:class/Auth_v1" and resource "RES001" and path "src/auth.py" without range
|
|
Then modifying the temporal node uri should raise an error
|
|
|
|
Scenario: TemporalNode rejects empty node_uri
|
|
Then creating a TemporalNode with empty node_uri should raise ValueError
|
|
Scenario: TemporalNode rejects whitespace-only node_uri
|
|
Then creating a TemporalNode with whitespace node_uri should raise ValueError
|
|
Scenario: TemporalNode rejects empty source_resource
|
|
Then creating a TemporalNode with empty source_resource should raise ValueError
|
|
Scenario: TemporalNode rejects empty source_path
|
|
Then creating a TemporalNode with empty source_path should raise ValueError
|
|
Scenario: TemporalNode rejects empty source_range
|
|
Then creating a TemporalNode with empty source_range should raise ValueError
|
|
Scenario: TemporalNode rejects whitespace-only source_range
|
|
Then creating a TemporalNode with whitespace source_range should raise ValueError
|
|
|
|
# ---- RevisionChain model ----
|
|
Scenario: Create a RevisionChain with no predecessors
|
|
Given a RevisionChain with only current_uri "uko-py:class/Auth_v1"
|
|
Then the revision chain depth should be 1
|
|
And the revision chain predecessors should be empty
|
|
And the revision chain all_uris should contain "uko-py:class/Auth_v1"
|
|
|
|
Scenario: Create a RevisionChain with predecessors
|
|
Given a RevisionChain with current_uri "uko-py:class/Auth_v3" and predecessors "uko-py:class/Auth_v1,uko-py:class/Auth_v2"
|
|
Then the revision chain depth should be 3
|
|
And the revision chain predecessors should have 2 entries
|
|
And the revision chain all_uris should have 3 entries
|
|
|
|
Scenario: RevisionChain is frozen
|
|
Given a RevisionChain with only current_uri "uko-py:class/Auth_v1"
|
|
Then modifying the revision chain current_uri should raise an error
|
|
|
|
Scenario: RevisionChain rejects empty current_uri
|
|
Then creating a RevisionChain with empty current_uri should raise ValueError
|
|
Scenario: RevisionChain rejects whitespace-only current_uri
|
|
Then creating a RevisionChain with whitespace current_uri should raise ValueError
|
|
Scenario: RevisionChain rejects empty predecessor URI
|
|
Then creating a RevisionChain with empty predecessor should raise ValueError
|
|
Scenario: RevisionChain rejects current_uri in predecessors
|
|
Then creating a RevisionChain with current_uri in predecessors should raise ValueError
|
|
Scenario: RevisionChain rejects duplicate predecessors
|
|
Then creating a RevisionChain with duplicate predecessors should raise ValueError
|
|
|
|
# ---- TierRetentionConfig model ----
|
|
Scenario: TierRetentionConfig defaults match spec
|
|
Given a TierRetentionConfig with defaults
|
|
Then the warm retention hours should be 24
|
|
And the cold retention days should be 90
|
|
|
|
Scenario: TierRetentionConfig is frozen
|
|
Given a TierRetentionConfig with defaults
|
|
Then modifying the retention warm hours should raise an error
|
|
|
|
Scenario: TierRetentionConfig rejects zero warm hours
|
|
Then creating a TierRetentionConfig with warm_retention_hours 0 should raise ValueError
|
|
Scenario: TierRetentionConfig rejects zero cold days
|
|
Then creating a TierRetentionConfig with cold_retention_days 0 should raise ValueError
|
|
|
|
# ---- TierQueryResult model ----
|
|
Scenario: Create a TierQueryResult with no nodes
|
|
Given a TierQueryResult for HOT tier with CURRENT scope
|
|
Then the tier query result nodes should be empty
|
|
And the tier query result tier should be HOT
|
|
And the tier query result temporal_scope should be CURRENT
|
|
|
|
Scenario: TierQueryResult is frozen
|
|
Given a TierQueryResult for HOT tier with CURRENT scope
|
|
Then modifying the tier query result tier should raise an error
|
|
|
|
# ---- TemporalBackend protocol ----
|
|
Scenario: InMemoryTemporalBackend satisfies TemporalBackend protocol
|
|
Given an InMemoryTemporalBackend instance
|
|
Then it should satisfy the TemporalBackend protocol
|
|
|
|
# ---- InMemoryTemporalBackend: store and get ----
|
|
Scenario: Store a node and get current
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
When I get the current node for base "uko-py:class/Auth"
|
|
Then the current node uri should be "uko-py:class/Auth_v1"
|
|
|
|
Scenario: Get current returns None when no match
|
|
Given an InMemoryTemporalBackend instance
|
|
When I get the current node for base "uko-py:class/NonExistent"
|
|
Then the current node should be None
|
|
|
|
Scenario: Get current does not match unrelated URI prefix
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/AuthManager_v1" for resource "RES001" at "src/auth.py"
|
|
When I get the current node for base "uko-py:class/Auth"
|
|
Then the current node should be None
|
|
|
|
# ---- InMemoryTemporalBackend: create revision ----
|
|
Scenario: Create a revision marks old node historical
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
When I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
Then the old node "uko-py:class/Auth_v1" should not be current
|
|
And the old node "uko-py:class/Auth_v1" should have valid_until set
|
|
And the new node "uko-py:class/Auth_v2" should be current
|
|
And the new node "uko-py:class/Auth_v2" should have is_revision_of "uko-py:class/Auth_v1"
|
|
|
|
Scenario: Create revision from nonexistent URI raises KeyError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then creating a revision from nonexistent "uko-py:class/Missing" should raise KeyError
|
|
Scenario: Create revision with blank URI raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then creating a revision from blank URI should raise ValueError
|
|
|
|
Scenario: Create revision with same URI as current raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
Then creating a revision from "uko-py:class/Auth_v1" to itself should raise ValueError
|
|
|
|
Scenario: Store node rejects duplicate URI
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
Then storing a duplicate node "uko-py:class/Auth_v1" should raise ValueError
|
|
|
|
Scenario: Create revision rejects URI that already exists
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And a stored temporal node "uko-py:class/Auth_v2" for resource "RES001" at "src/auth.py"
|
|
Then creating a revision from "uko-py:class/Auth_v1" to existing "uko-py:class/Auth_v2" should raise ValueError
|
|
|
|
Scenario: Create revision rejects wrong is_revision_of on new node
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
Then creating a revision from "uko-py:class/Auth_v1" with wrong is_revision_of should raise ValueError
|
|
Scenario: Create revision rejects is_current false on new node
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
Then creating a revision from "uko-py:class/Auth_v1" with is_current false should raise ValueError
|
|
|
|
# ---- InMemoryTemporalBackend: get history ----
|
|
Scenario: Get history with ALL scope returns all versions
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I get the history for base "uko-py:class/Auth" with scope ALL
|
|
Then the history should have 2 nodes
|
|
And the history should be ordered newest first
|
|
|
|
Scenario: Get history with CURRENT scope returns only current
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I get the history for base "uko-py:class/Auth" with scope CURRENT
|
|
Then the history should have 1 nodes
|
|
|
|
Scenario: Get history with blank base URI raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then getting history for blank base URI should raise ValueError
|
|
|
|
# ---- InMemoryTemporalBackend: revision chain ----
|
|
Scenario: Get revision chain for single node
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
When I get the revision chain for "uko-py:class/Auth_v1"
|
|
Then the revision chain should have depth 1
|
|
And the revision chain current should be "uko-py:class/Auth_v1"
|
|
|
|
Scenario: Get revision chain after multiple revisions
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
And I create a revision from "uko-py:class/Auth_v2" to "uko-py:class/Auth_v3"
|
|
When I get the revision chain for "uko-py:class/Auth_v3"
|
|
Then the revision chain should have depth 3
|
|
And the revision chain current should be "uko-py:class/Auth_v3"
|
|
And the revision chain predecessors should include "uko-py:class/Auth_v1"
|
|
And the revision chain predecessors should include "uko-py:class/Auth_v2"
|
|
|
|
Scenario: Get revision chain from middle node includes full chain
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
And I create a revision from "uko-py:class/Auth_v2" to "uko-py:class/Auth_v3"
|
|
When I get the revision chain for "uko-py:class/Auth_v1"
|
|
Then the revision chain should have depth 3
|
|
|
|
Scenario: Get revision chain for nonexistent URI raises KeyError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then getting revision chain for nonexistent URI should raise KeyError
|
|
|
|
Scenario: Fully-historical revision chain raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node with all nodes historical
|
|
Then getting revision chain for fully-historical node should raise ValueError
|
|
|
|
# ---- InMemoryTemporalBackend: query by tier ----
|
|
Scenario: Hot tier query returns only current nodes
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I query the HOT tier
|
|
Then the tier query should return 1 node
|
|
And the tier query node should be current
|
|
|
|
Scenario: Cold tier query returns all temporal versions
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I query the COLD tier
|
|
Then the tier query should return 2 nodes
|
|
|
|
# ---- InMemoryTemporalBackend: mark historical ----
|
|
Scenario: Mark a node as historical
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
When I mark "uko-py:class/Auth_v1" as historical
|
|
Then the marked node should not be current
|
|
And the marked node should have valid_until set
|
|
|
|
Scenario: Mark historical with blank URI raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then marking blank URI as historical should raise ValueError
|
|
Scenario: Mark historical with nonexistent URI raises KeyError
|
|
Given an InMemoryTemporalBackend instance
|
|
Then marking nonexistent URI as historical should raise KeyError
|
|
Scenario: Mark historical on already-historical node raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I mark "uko-py:class/Auth_v1" as historical
|
|
Then marking already-historical "uko-py:class/Auth_v1" should raise ValueError
|
|
|
|
Scenario: Create revision on already-historical node raises ValueError
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
Then creating a revision from historical "uko-py:class/Auth_v1" to "uko-py:class/Auth_v3" should raise ValueError
|
|
|
|
Scenario: Get revision chain identifies current head explicitly
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I get the revision chain for "uko-py:class/Auth_v1"
|
|
Then the revision chain current_uri should be "uko-py:class/Auth_v2"
|
|
And the revision chain predecessors should contain "uko-py:class/Auth_v1"
|
|
|
|
Scenario: Get revision chain excludes sibling branch nodes
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node with branching revision chain
|
|
When I get the revision chain for "uko-py:class/Branch_B"
|
|
Then the revision chain should not contain "uko-py:class/Branch_C"
|
|
And the revision chain current_uri should be "uko-py:class/Branch_B"
|
|
|
|
# ---- InMemoryTemporalBackend: diff-coverage scenarios ----
|
|
Scenario: Get history with RECENT scope returns current and recently expired nodes
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I get the history for base "uko-py:class/Auth" with scope RECENT
|
|
Then the history should have 2 nodes
|
|
|
|
Scenario: Revision chain with cycle guard stops traversal
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node with cyclic revision chain
|
|
When I get the revision chain for "uko-py:class/Cycle_B"
|
|
Then the revision chain should have depth 2
|
|
|
|
Scenario: Revision chain with missing predecessor stops traversal
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node with dangling predecessor
|
|
When I get the revision chain for "uko-py:class/Dangling_B"
|
|
Then the revision chain should have depth 1
|
|
Scenario: Warm tier query returns current and recently expired nodes
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I query the WARM tier
|
|
Then the tier query should return 2 nodes
|
|
|
|
Scenario: Query by tier with RECENT scope filters within tier pool
|
|
Given an InMemoryTemporalBackend instance
|
|
And a stored temporal node "uko-py:class/Auth_v1" for resource "RES001" at "src/auth.py"
|
|
And I create a revision from "uko-py:class/Auth_v1" to "uko-py:class/Auth_v2"
|
|
When I query the HOT tier with scope RECENT
|
|
Then the tier query should return 1 node
|
|
# ---- TemporalService ----
|
|
Scenario: TemporalService store initial node
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
When I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
Then the stored node should be current
|
|
And the stored node should have no predecessor
|
|
|
|
Scenario: TemporalService create revision
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py"
|
|
Then the service revision node should be current
|
|
And the service revision node should have is_revision_of "uko-py:class/Foo_v1"
|
|
|
|
Scenario: TemporalService get current
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I get the service current for "uko-py:class/Foo"
|
|
Then the service current node uri should be "uko-py:class/Foo_v1"
|
|
|
|
Scenario: TemporalService query by tier uses default scope
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I query HOT tier via the service
|
|
Then the service tier query scope should be CURRENT
|
|
|
|
Scenario: TemporalService query by tier allows scope override
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I query HOT tier via the service with scope ALL
|
|
Then the service tier query scope should be ALL
|
|
|
|
Scenario: TemporalService mark historical
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I mark "uko-py:class/Foo_v1" as historical via the service
|
|
Then the service marked node should not be current
|
|
|
|
Scenario: TemporalService rejects None backend
|
|
Then creating a TemporalService with None backend should raise TypeError
|
|
|
|
Scenario: TemporalService rejects blank node_uri
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
Then storing an initial node with blank URI via the service should raise ValueError
|
|
Scenario: TemporalService rejects blank source_resource
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
Then storing an initial node with blank source_resource via the service should raise ValueError
|
|
|
|
Scenario: TemporalService get history returns matching versions
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py"
|
|
And I get the service history for "uko-py:class/Foo" with scope ALL
|
|
Then the service history should have 2 nodes
|
|
|
|
Scenario: TemporalService get history with CURRENT scope returns only current
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py"
|
|
And I get the service history for "uko-py:class/Foo" with scope CURRENT
|
|
Then the service history should have 1 nodes
|
|
|
|
Scenario: TemporalService get history rejects blank base URI
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
Then getting service history for blank base URI should raise ValueError
|
|
Scenario: TemporalService get revision chain returns full chain
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py"
|
|
And I get the service revision chain for "uko-py:class/Foo_v2"
|
|
Then the service revision chain depth should be 2
|
|
And the service revision chain current should be "uko-py:class/Foo_v2"
|
|
|
|
Scenario: TemporalService get revision chain rejects blank URI
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
Then getting service revision chain for blank URI should raise ValueError
|
|
Scenario: TemporalService query COLD with CURRENT scope returns only current
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I create a service revision from "uko-py:class/Foo_v1" to "uko-py:class/Foo_v2" for resource "RES001" at "src/foo.py"
|
|
And I query COLD tier via the service with scope CURRENT
|
|
Then the service tier query should return 1 node
|
|
And the service tier query node should be current
|
|
|
|
Scenario: TemporalService default retention matches spec
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
Then the service retention warm hours should be 24
|
|
And the service retention cold days should be 90
|
|
|
|
Scenario: TemporalService WARM tier uses RECENT default scope
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I query WARM tier via the service
|
|
Then the service tier query scope should be RECENT
|
|
|
|
Scenario: TemporalService COLD tier uses ALL default scope
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I query COLD tier via the service
|
|
Then the service tier query scope should be ALL
|
|
|
|
Scenario: TemporalService create revision with same URI raises ValueError
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
Then creating a service revision from "uko-py:class/Foo_v1" to itself should raise ValueError
|
|
Scenario: TemporalService mark historical on already-historical raises ValueError
|
|
Given a TemporalService with InMemoryTemporalBackend
|
|
And I store an initial node "uko-py:class/Foo_v1" for resource "RES001" at "src/foo.py"
|
|
When I mark "uko-py:class/Foo_v1" as historical via the service
|
|
Then marking "uko-py:class/Foo_v1" as historical again via the service should raise ValueError
|
|
|
|
# ---- BackendSet temporal typing ----
|
|
Scenario: BackendSet accepts TemporalBackend
|
|
Given an InMemoryTemporalBackend instance
|
|
When I create a BackendSet with the temporal backend
|
|
Then the BackendSet temporal field should not be None
|
|
|
|
Scenario: StrategyConfig extra deep-freezes nested dicts
|
|
Given a StrategyConfig with nested extra dict
|
|
Then mutating the nested extra dict should raise TypeError
|
|
|
|
Scenario: StrategyConfig extra deep-freezes nested lists to tuples
|
|
Given a StrategyConfig with nested list in extra
|
|
Then the nested list should be frozen as a tuple
|
|
|
|
# ---- Structural assertions (Phase 3.1) ----
|
|
Scenario: TemporalMetadata has all spec-required fields
|
|
Then TemporalMetadata should have field "valid_from"
|
|
And TemporalMetadata should have field "valid_until"
|
|
And TemporalMetadata should have field "is_current"
|
|
And TemporalMetadata should have field "is_revision_of"
|
|
Scenario: TemporalNode has all spec-required fields
|
|
Then TemporalNode should have field "node_uri"
|
|
And TemporalNode should have field "source_resource"
|
|
And TemporalNode should have field "source_path"
|
|
And TemporalNode should have field "source_range"
|
|
And TemporalNode should have field "temporal"
|
|
Scenario: RevisionChain has all spec-required fields
|
|
Then RevisionChain should have field "current_uri"
|
|
And RevisionChain should have field "predecessors"
|
|
Scenario: TierRetentionConfig has all spec-required fields
|
|
Then TierRetentionConfig should have field "warm_retention_hours"
|
|
And TierRetentionConfig should have field "cold_retention_days"
|
|
Scenario: TierQueryResult has all spec-required fields
|
|
Then TierQueryResult should have field "nodes"
|
|
And TierQueryResult should have field "tier"
|
|
And TierQueryResult should have field "temporal_scope"
|