generated from coulomb/repo-seed
Structured OperationFailure, BatchItemResult, and BatchOperationResult envelopes
This commit is contained in:
@@ -7,11 +7,15 @@ from kontextual_engine import (
|
||||
ActorType,
|
||||
AssetRegistryService,
|
||||
AssetRepresentation,
|
||||
AssetVersion,
|
||||
AuthorizationError,
|
||||
AuditEvent,
|
||||
AuditOutcome,
|
||||
Classification,
|
||||
ContextEntity,
|
||||
ContextEntityType,
|
||||
InMemoryAssetRegistryRepository,
|
||||
IngestionJob,
|
||||
LifecycleState,
|
||||
MetadataFieldDefinition,
|
||||
MetadataRecord,
|
||||
@@ -25,6 +29,7 @@ from kontextual_engine import (
|
||||
SourceReference,
|
||||
SQLiteAssetRegistryRepository,
|
||||
ValidationError,
|
||||
VersionChangeType,
|
||||
)
|
||||
|
||||
|
||||
@@ -343,6 +348,117 @@ def test_asset_registry_validates_metadata_schema_before_writes() -> None:
|
||||
assert [record.key for record in repo.list_metadata_records(created.asset.id)] == ["owner"]
|
||||
|
||||
|
||||
def test_asset_registry_metadata_batch_reports_partial_failures() -> None:
|
||||
repo = InMemoryAssetRegistryRepository()
|
||||
schema = MetadataSchema(
|
||||
schema_id="schema-batch-note-v1",
|
||||
name="Batch Note Metadata",
|
||||
asset_types=("batch-note",),
|
||||
allow_unknown=False,
|
||||
fields=(
|
||||
MetadataFieldDefinition("owner", MetadataValueType.STRING, required=True, require_confirmed=True),
|
||||
MetadataFieldDefinition("priority", MetadataValueType.INTEGER, allow_multiple=True, min_value=1, max_value=5),
|
||||
),
|
||||
)
|
||||
service = AssetRegistryService(repo, metadata_schemas=[schema])
|
||||
context = operation_context()
|
||||
created = service.create_asset(
|
||||
"Batch Note",
|
||||
Classification(asset_type="batch-note", sensitivity=Sensitivity.INTERNAL),
|
||||
context,
|
||||
asset_id="asset-batch-note",
|
||||
metadata_records=[MetadataRecord("owner", "Platform Knowledge", confirmed=True)],
|
||||
)
|
||||
|
||||
result = service.add_metadata_records_batch(
|
||||
created.asset.id,
|
||||
(
|
||||
MetadataRecord("priority", 3, record_id="meta-priority-ok"),
|
||||
MetadataRecord("priority", 9, record_id="meta-priority-too-large"),
|
||||
MetadataRecord("phase", "beta", record_id="meta-phase-unknown"),
|
||||
),
|
||||
context,
|
||||
expected_current_version_id=created.version.version_id,
|
||||
)
|
||||
|
||||
assert result.total == 3
|
||||
assert result.succeeded == 1
|
||||
assert result.failed == 2
|
||||
assert result.partial is True
|
||||
assert result.outcome == "partial"
|
||||
assert [item.success for item in result.items] == [True, False, False]
|
||||
assert result.items[0].result_ref["record_id"] == "meta-priority-ok"
|
||||
assert result.items[1].error is not None
|
||||
assert result.items[1].error.code == "kontextual.validation"
|
||||
assert result.items[1].error.correlation_id == "corr-test"
|
||||
assert "metadata schema" in result.items[1].error.remediation
|
||||
assert {issue["code"] for issue in result.items[1].error.details["issues"]} == {
|
||||
"metadata.value_too_large"
|
||||
}
|
||||
assert result.items[2].error is not None
|
||||
assert {issue["code"] for issue in result.items[2].error.details["issues"]} == {
|
||||
"metadata.unknown_field"
|
||||
}
|
||||
assert result.to_dict()["audit_event_id"] == result.audit_event_id
|
||||
|
||||
metadata_records = repo.list_metadata_records(created.asset.id)
|
||||
versions = repo.list_versions(created.asset.id)
|
||||
events = repo.list_audit_events(target=f"asset:{created.asset.id}")
|
||||
|
||||
assert [record.key for record in metadata_records] == ["owner", "priority"]
|
||||
assert metadata_records[1].record_id == "meta-priority-ok"
|
||||
assert [version.sequence for version in versions] == [1, 2]
|
||||
assert versions[-1].metadata_delta == {"priority": 3}
|
||||
assert [event.operation for event in events] == [
|
||||
"asset.create",
|
||||
"asset.metadata.add",
|
||||
"asset.metadata.batch_add",
|
||||
]
|
||||
assert events[-1].outcome.value == "partial"
|
||||
assert events[-1].correlation_id == "corr-test"
|
||||
assert events[-1].details["total"] == 3
|
||||
assert events[-1].details["succeeded"] == 1
|
||||
assert events[-1].details["failed"] == 2
|
||||
assert events[-1].details["failed_item_ids"] == [
|
||||
"meta-priority-too-large",
|
||||
"meta-phase-unknown",
|
||||
]
|
||||
|
||||
|
||||
def test_asset_registry_metadata_batch_rejects_stale_expected_version_before_writes() -> None:
|
||||
repo = InMemoryAssetRegistryRepository()
|
||||
service = AssetRegistryService(repo)
|
||||
context = operation_context()
|
||||
created = service.create_asset(
|
||||
"Batch Conflict",
|
||||
Classification(asset_type="note", sensitivity=Sensitivity.INTERNAL),
|
||||
context,
|
||||
asset_id="asset-batch-conflict",
|
||||
)
|
||||
service.add_metadata_record(
|
||||
created.asset.id,
|
||||
MetadataRecord("owner", "Platform Knowledge", confirmed=True),
|
||||
context,
|
||||
expected_current_version_id=created.version.version_id,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
service.add_metadata_records_batch(
|
||||
created.asset.id,
|
||||
(MetadataRecord("topic", "architecture"),),
|
||||
context,
|
||||
expected_current_version_id=created.version.version_id,
|
||||
)
|
||||
|
||||
assert exc_info.value.details["code"] == "asset.version_conflict"
|
||||
assert exc_info.value.details["operation"] == "asset.metadata.batch_add"
|
||||
assert [record.key for record in repo.list_metadata_records(created.asset.id)] == ["owner"]
|
||||
assert [event.operation for event in repo.list_audit_events(target=f"asset:{created.asset.id}")] == [
|
||||
"asset.create",
|
||||
"asset.metadata.add",
|
||||
]
|
||||
|
||||
|
||||
def test_asset_registry_applies_persisted_metadata_schema_assignments() -> None:
|
||||
repo = InMemoryAssetRegistryRepository()
|
||||
service = AssetRegistryService(repo)
|
||||
@@ -662,6 +778,54 @@ def test_sqlite_registry_filters_assets_after_reload(tmp_path: Path) -> None:
|
||||
] == ["asset-guide"]
|
||||
|
||||
|
||||
def test_sqlite_registry_persists_metadata_batch_partial_audit_after_reload(tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "registry.sqlite"
|
||||
repo = SQLiteAssetRegistryRepository(db_path)
|
||||
schema = MetadataSchema(
|
||||
schema_id="schema-batch-ticket-v1",
|
||||
name="Batch Ticket Metadata",
|
||||
asset_types=("ticket",),
|
||||
allow_unknown=False,
|
||||
fields=(
|
||||
MetadataFieldDefinition("owner", MetadataValueType.STRING, required=True, require_confirmed=True),
|
||||
MetadataFieldDefinition("severity", MetadataValueType.INTEGER, allow_multiple=True, min_value=1, max_value=5),
|
||||
),
|
||||
)
|
||||
service = AssetRegistryService(repo, metadata_schemas=[schema])
|
||||
context = operation_context()
|
||||
created = service.create_asset(
|
||||
"Batch Ticket",
|
||||
Classification(asset_type="ticket", sensitivity=Sensitivity.INTERNAL),
|
||||
context,
|
||||
asset_id="asset-batch-ticket",
|
||||
metadata_records=[MetadataRecord("owner", "Operations", confirmed=True)],
|
||||
)
|
||||
|
||||
result = service.add_metadata_records_batch(
|
||||
created.asset.id,
|
||||
(
|
||||
MetadataRecord("severity", 4, record_id="meta-severity-ok"),
|
||||
MetadataRecord("severity", 9, record_id="meta-severity-too-large"),
|
||||
),
|
||||
context,
|
||||
expected_current_version_id=created.version.version_id,
|
||||
)
|
||||
reloaded = SQLiteAssetRegistryRepository(db_path)
|
||||
|
||||
assert result.outcome == "partial"
|
||||
assert result.audit_event_id is not None
|
||||
assert [record.key for record in reloaded.list_metadata_records(created.asset.id)] == [
|
||||
"owner",
|
||||
"severity",
|
||||
]
|
||||
assert reloaded.list_metadata_records(created.asset.id)[1].record_id == "meta-severity-ok"
|
||||
assert [version.sequence for version in reloaded.list_versions(created.asset.id)] == [1, 2]
|
||||
assert reloaded.get_audit_event(result.audit_event_id).outcome == AuditOutcome.PARTIAL
|
||||
assert reloaded.get_audit_event(result.audit_event_id).details["failed_item_ids"] == [
|
||||
"meta-severity-too-large"
|
||||
]
|
||||
|
||||
|
||||
def test_sqlite_registry_enforces_representation_asset_reference(tmp_path: Path) -> None:
|
||||
repo = SQLiteAssetRegistryRepository(tmp_path / "registry.sqlite")
|
||||
representation = AssetRepresentation.from_content(
|
||||
@@ -675,6 +839,39 @@ def test_sqlite_registry_enforces_representation_asset_reference(tmp_path: Path)
|
||||
repo.save_representation(representation)
|
||||
|
||||
|
||||
def test_sqlite_registry_enforces_durable_reference_integrity(tmp_path: Path) -> None:
|
||||
repo = SQLiteAssetRegistryRepository(tmp_path / "registry.sqlite")
|
||||
|
||||
with pytest.raises(ValidationError, match="unknown asset"):
|
||||
repo.save_version(
|
||||
AssetVersion(
|
||||
asset_id="asset-missing",
|
||||
sequence=1,
|
||||
change_type=VersionChangeType.CREATED,
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match="unknown actor"):
|
||||
repo.save_audit_event(
|
||||
AuditEvent(
|
||||
operation="asset.create",
|
||||
target="asset:asset-missing",
|
||||
outcome=AuditOutcome.SUCCESS,
|
||||
actor_id="actor-missing",
|
||||
correlation_id="corr-missing",
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError, match="unknown actor"):
|
||||
repo.save_ingestion_job(
|
||||
IngestionJob.create(
|
||||
input={"connector": "local_file", "source_uri": "missing.txt"},
|
||||
actor_id="actor-missing",
|
||||
correlation_id="corr-missing",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def operation_context() -> OperationContext:
|
||||
actor = Actor.create(
|
||||
ActorType.HUMAN,
|
||||
|
||||
Reference in New Issue
Block a user