generated from coulomb/repo-seed
Governed asset registry slice with asset creation, representations, metadata, lifecycle transitions, policy authorization, fail-closed denial, audit events, and version records
This commit is contained in:
182
tests/test_asset_registry.py
Normal file
182
tests/test_asset_registry.py
Normal file
@@ -0,0 +1,182 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from kontextual_engine import (
|
||||
Actor,
|
||||
ActorType,
|
||||
AssetRegistryService,
|
||||
AssetRepresentation,
|
||||
AuthorizationError,
|
||||
Classification,
|
||||
InMemoryAssetRegistryRepository,
|
||||
LifecycleState,
|
||||
MetadataRecord,
|
||||
OperationContext,
|
||||
PolicyDecision,
|
||||
RepresentationKind,
|
||||
Sensitivity,
|
||||
SourceReference,
|
||||
SQLiteAssetRegistryRepository,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
|
||||
def test_asset_registry_service_creates_assets_with_versions_and_audit() -> None:
|
||||
repo = InMemoryAssetRegistryRepository()
|
||||
service = AssetRegistryService(repo)
|
||||
context = operation_context()
|
||||
source_ref = SourceReference(
|
||||
source_system="repo",
|
||||
path="docs/intent.md",
|
||||
checksum="sha256:source",
|
||||
)
|
||||
representation = AssetRepresentation.from_content(
|
||||
"asset-intent",
|
||||
RepresentationKind.SOURCE,
|
||||
"text/markdown",
|
||||
"# Intent\n",
|
||||
storage_ref="object://intent-source",
|
||||
source_ref_id=source_ref.id,
|
||||
)
|
||||
metadata = MetadataRecord(
|
||||
"topic",
|
||||
"architecture",
|
||||
provenance={"producer": "human"},
|
||||
confirmed=True,
|
||||
)
|
||||
|
||||
result = service.create_asset(
|
||||
"Intent",
|
||||
Classification(
|
||||
asset_type="document",
|
||||
sensitivity=Sensitivity.INTERNAL,
|
||||
owner="Platform Knowledge",
|
||||
),
|
||||
context,
|
||||
asset_id="asset-intent",
|
||||
source_refs=[source_ref],
|
||||
representations=[representation],
|
||||
metadata_records=[metadata],
|
||||
)
|
||||
|
||||
assert result.asset.id == "asset-intent"
|
||||
assert result.asset.current_version_id == result.version.version_id
|
||||
assert result.version.sequence == 1
|
||||
assert result.audit_event.outcome.value == "success"
|
||||
assert result.policy_decision.allowed is True
|
||||
assert repo.get_asset("asset-intent").source_refs[0].path == "docs/intent.md"
|
||||
assert repo.list_representations(asset_id="asset-intent")[0].storage_ref == "object://intent-source"
|
||||
assert repo.list_metadata_records("asset-intent")[0].confirmed is True
|
||||
assert repo.list_audit_events(target="asset:asset-intent")[0].operation == "asset.create"
|
||||
|
||||
|
||||
def test_asset_registry_lifecycle_policy_denial_fails_closed_and_audits() -> None:
|
||||
repo = InMemoryAssetRegistryRepository()
|
||||
service = AssetRegistryService(repo, policy_gateway=DenyLifecyclePolicy())
|
||||
context = operation_context()
|
||||
created = service.create_asset(
|
||||
"Governed Asset",
|
||||
Classification(asset_type="document", sensitivity=Sensitivity.CONFIDENTIAL),
|
||||
context,
|
||||
asset_id="asset-governed",
|
||||
)
|
||||
|
||||
with pytest.raises(AuthorizationError) as exc_info:
|
||||
service.transition_lifecycle(created.asset.id, LifecycleState.RETIRED, context)
|
||||
|
||||
events = repo.list_audit_events(target="asset:asset-governed")
|
||||
|
||||
assert exc_info.value.details["correlation_id"] == "corr-test"
|
||||
assert exc_info.value.details["policy_decision"]["effect"] == "fail_closed"
|
||||
assert [event.outcome.value for event in events] == ["success", "denied"]
|
||||
assert repo.get_asset("asset-governed").lifecycle == LifecycleState.ACTIVE
|
||||
|
||||
|
||||
def test_sqlite_asset_registry_survives_reinstantiation(tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "registry.sqlite"
|
||||
repo = SQLiteAssetRegistryRepository(db_path)
|
||||
service = AssetRegistryService(repo)
|
||||
context = operation_context()
|
||||
source_ref = SourceReference(source_system="repo", path="README.md", checksum="sha256:readme")
|
||||
source = AssetRepresentation.from_content(
|
||||
"asset-readme",
|
||||
RepresentationKind.SOURCE,
|
||||
"text/markdown",
|
||||
"# Readme\n",
|
||||
storage_ref="object://readme-source",
|
||||
)
|
||||
created = service.create_asset(
|
||||
"Readme",
|
||||
Classification(asset_type="document", sensitivity=Sensitivity.PUBLIC),
|
||||
context,
|
||||
asset_id="asset-readme",
|
||||
source_refs=[source_ref],
|
||||
representations=[source],
|
||||
)
|
||||
service.add_metadata_record(
|
||||
created.asset.id,
|
||||
MetadataRecord("owner", "Platform Knowledge", confirmed=True),
|
||||
context,
|
||||
)
|
||||
service.request_delete(created.asset.id, context)
|
||||
|
||||
reloaded = SQLiteAssetRegistryRepository(db_path)
|
||||
asset = reloaded.get_asset("asset-readme")
|
||||
|
||||
assert asset.lifecycle == LifecycleState.DELETE_REQUESTED
|
||||
assert asset.source_refs[0].path == "README.md"
|
||||
assert [item.kind for item in reloaded.list_representations(asset_id=asset.id)] == [
|
||||
RepresentationKind.SOURCE
|
||||
]
|
||||
assert [item.key for item in reloaded.list_metadata_records(asset.id)] == ["owner"]
|
||||
assert [version.sequence for version in reloaded.list_versions(asset.id)] == [1, 2, 3]
|
||||
assert [event.operation for event in reloaded.list_audit_events(target="asset:asset-readme")] == [
|
||||
"asset.create",
|
||||
"asset.metadata.add",
|
||||
"asset.lifecycle.transition",
|
||||
]
|
||||
|
||||
|
||||
def test_sqlite_registry_enforces_representation_asset_reference(tmp_path: Path) -> None:
|
||||
repo = SQLiteAssetRegistryRepository(tmp_path / "registry.sqlite")
|
||||
representation = AssetRepresentation.from_content(
|
||||
"missing-asset",
|
||||
RepresentationKind.NORMALIZED,
|
||||
"text/plain",
|
||||
"normalized",
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match="unknown asset"):
|
||||
repo.save_representation(representation)
|
||||
|
||||
|
||||
def operation_context() -> OperationContext:
|
||||
actor = Actor.create(
|
||||
ActorType.HUMAN,
|
||||
actor_id="user-test",
|
||||
display_name="Test User",
|
||||
groups=["engineering"],
|
||||
)
|
||||
return OperationContext.create(actor, correlation_id="corr-test")
|
||||
|
||||
|
||||
class DenyLifecyclePolicy:
|
||||
def authorize(
|
||||
self,
|
||||
context: OperationContext,
|
||||
action: str,
|
||||
resource: str,
|
||||
*,
|
||||
resource_metadata: dict[str, str] | None = None,
|
||||
) -> PolicyDecision:
|
||||
if action == "asset.lifecycle.transition":
|
||||
return PolicyDecision.fail_closed(
|
||||
context.actor.id,
|
||||
action,
|
||||
resource,
|
||||
reason="lifecycle transitions require review",
|
||||
context={"resource_metadata": resource_metadata or {}},
|
||||
)
|
||||
return PolicyDecision.allow(context.actor.id, action, resource)
|
||||
|
||||
Reference in New Issue
Block a user