CMIS layer into an honest CMIS 1.1

This commit is contained in:
2026-05-07 04:11:09 +02:00
parent ebace73761
commit 7855a8bfd0
13 changed files with 498 additions and 87 deletions

View File

@@ -20,7 +20,8 @@
{
"capability_group": "navigation",
"tck_groups": ["NavigationTestGroup"],
"expected": "selected-pass"
"expected": "partial-pass",
"known_gaps": ["get_descendants", "get_folder_tree", "multifiling", "unfiling"]
},
{
"capability_group": "object-content",
@@ -30,14 +31,14 @@
{
"capability_group": "versioning",
"tck_groups": ["VersioningTestGroup"],
"expected": "partial-pass",
"known_gaps": ["private_working_copy"]
"expected": "skip",
"known_gaps": ["versioning_services", "private_working_copy", "all_versions_search"]
},
{
"capability_group": "discovery-query",
"tck_groups": ["QueryTestGroup"],
"expected": "partial-pass",
"known_gaps": ["full_cmis_sql_joins"]
"known_gaps": ["full_cmis_sql_joins", "order_by"]
},
{
"capability_group": "relationships",
@@ -48,7 +49,7 @@
"capability_group": "acl-policy",
"tck_groups": ["AclTestGroup", "PolicyTestGroup"],
"expected": "partial-pass",
"known_gaps": ["apply_policy", "remove_policy"]
"known_gaps": ["apply_acl", "apply_policy", "remove_policy"]
},
{
"capability_group": "change-log",
@@ -63,4 +64,3 @@
}
]
}

View File

@@ -56,6 +56,7 @@ def test_governed_authoring_profile_allows_selected_write_actions() -> None:
assert profile.decide_action(CMISAction.CREATE_DOCUMENT, context).allowed is True
assert profile.decide_action(CMISAction.SET_CONTENT_STREAM, context).allowed is True
assert profile.decide_action(CMISAction.BULK_UPDATE_PROPERTIES, context).allowed is False
assert profile.decide_action(CMISAction.APPLY_ACL, context).reason == "cmis_operation_not_implemented"
def test_profiles_hide_denied_sensitivities_without_partial_exposure() -> None:
@@ -148,4 +149,3 @@ def test_disabled_access_point_denies_all_actions_and_visibility() -> None:
assert access_point.decide_action(CMISAction.GET_OBJECT, context).reason == "cmis_access_point_disabled"
assert access_point.exposes_asset(_asset("asset-public", "public"), context) is False

View File

@@ -100,6 +100,8 @@ def test_cmis_repository_info_and_type_definitions(cmis_client) -> None:
assert repository["repository_id"] == "kontextual-readonly-browser"
assert repository["cmis_version_supported"] == "1.1"
assert repository["capabilities"]["capability_query"] == "metadataonly"
assert repository["capabilities"]["capability_get_descendants"] is False
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only"
assert {item["base_type_id"] for item in types["items"]} >= {
"cmis:document",
"cmis:folder",
@@ -208,3 +210,14 @@ def test_cmis_readonly_route_rejects_mutation(cmis_client) -> None:
)
assert response.status_code == 403
def test_cmis_rejects_unsupported_standard_property_update_with_diagnostics(cmis_client) -> None:
response = cmis_client.post(
"/cmis/governed-authoring/browser/object/cmis:asset:asset-source/properties",
json={"properties": {"cmis:name": "Renamed"}},
)
assert response.status_code == 422
assert response.json()["detail"]["details"]["property"] == "cmis:name"
assert response.json()["detail"]["details"]["supported"] == ["kontextual:metadata:<key>"]

View File

@@ -0,0 +1,145 @@
from __future__ import annotations
from kontextual_engine import (
Actor,
ActorType,
CMISAccessPoint,
CMISAccessProfile,
CMISAction,
CMISBaseType,
CMISCapability,
CMISDomainMapper,
Classification,
KnowledgeAsset,
LifecycleState,
OperationContext,
)
EXPECTED_CAPABILITY_KEYS = {
"capability_content_stream_updatability",
"capability_changes",
"capability_renditions",
"capability_get_descendants",
"capability_get_folder_tree",
"capability_order_by",
"capability_multifiling",
"capability_unfiling",
"capability_version_specific_filing",
"capability_pwc_searchable",
"capability_pwc_updatable",
"capability_all_versions_searchable",
"capability_query",
"capability_join",
"capability_acl",
"capability_new_type_settable_attributes",
}
def _context() -> OperationContext:
return OperationContext.create(
Actor.create(ActorType.HUMAN, actor_id="actor-cmis-compliance"),
correlation_id="corr-cmis-compliance",
)
def _mapper(profile: CMISAccessProfile | None = None) -> CMISDomainMapper:
return CMISDomainMapper(
CMISAccessPoint(
access_point_id="cmis-compliance",
repository_id="kontextual-compliance",
profile=profile or CMISAccessProfile.readonly_browser(),
base_path="/cmis/compliance/browser",
metadata={"repository_name": "Kontextual Compliance Repository"},
)
)
def test_repository_info_uses_complete_conservative_cmis_11_capability_flags() -> None:
repository = _mapper().repository_info()
capabilities = repository["capabilities"]
assert EXPECTED_CAPABILITY_KEYS <= set(capabilities)
assert capabilities["capability_content_stream_updatability"] == "none"
assert capabilities["capability_changes"] == "objectidsonly"
assert capabilities["capability_renditions"] == "none"
assert capabilities["capability_get_descendants"] is False
assert capabilities["capability_get_folder_tree"] is False
assert capabilities["capability_order_by"] == "none"
assert capabilities["capability_multifiling"] is False
assert capabilities["capability_unfiling"] is False
assert capabilities["capability_version_specific_filing"] is False
assert capabilities["capability_pwc_searchable"] is False
assert capabilities["capability_pwc_updatable"] is False
assert capabilities["capability_all_versions_searchable"] is False
assert capabilities["capability_query"] == "metadataonly"
assert capabilities["capability_join"] == "none"
assert capabilities["capability_acl"] == "discover"
assert set(capabilities["capability_new_type_settable_attributes"].values()) == {False}
def test_authoring_profile_only_advertises_supported_content_write_semantics() -> None:
repository = _mapper(CMISAccessProfile.governed_authoring()).repository_info()
capabilities = repository["capabilities"]
assert capabilities["capability_content_stream_updatability"] == "anytime"
assert capabilities["capability_multifiling"] is False
assert capabilities["capability_renditions"] == "none"
def test_repository_info_exposes_nonstandard_projection_feature_and_unsupported_catalog() -> None:
repository = _mapper().repository_info()
feature_ids = {feature["id"] for feature in repository["repository_features"]}
unsupported = repository["unsupported_features"]
assert "urn:kontextual:cmis:feature:projection-parentage" in feature_ids
assert unsupported["multifiling"]["status"] == "projection_only"
assert unsupported["multifiling"]["standard_flag"] == "capability_multifiling"
assert unsupported["get_descendants"]["standard_flag"] == "capability_get_descendants"
assert unsupported["rendition_streams"]["standard_flag"] == "capability_renditions"
assert repository["compliance"]["posture"] == "declared-browser-binding-subset"
def test_type_definitions_do_not_claim_unimplemented_cmis_services() -> None:
types = {definition["base_type_id"]: definition for definition in _mapper().type_definitions()}
document = types[CMISBaseType.DOCUMENT.value]
folder = types[CMISBaseType.FOLDER.value]
relationship = types[CMISBaseType.RELATIONSHIP.value]
policy = types[CMISBaseType.POLICY.value]
assert document["versionable"] is False
assert document["controllable_acl"] is True
assert document["queryable"] is True
assert document["creatable"] is False
assert "cmis:contentStreamLength" in document["property_definitions"]
assert folder["controllable_acl"] is False
assert folder["queryable"] is False
assert relationship["creatable"] is False
assert relationship["queryable"] is False
assert policy["controllable_policy"] is False
def test_profile_denies_unimplemented_mutations_even_when_related_capability_exists() -> None:
profile = CMISAccessProfile(
name="acl-manager-shape",
capabilities=(CMISCapability.ACL,),
allow_mutations=True,
)
decision = profile.decide_action(CMISAction.APPLY_ACL, _context())
assert decision.allowed is False
assert decision.reason == "cmis_operation_not_implemented"
def test_deleted_assets_are_not_exposed_as_live_cmis_objects() -> None:
profile = CMISAccessProfile.readonly_browser()
asset = KnowledgeAsset.create(
"Deleted",
Classification(asset_type="document", sensitivity="internal"),
asset_id="asset-deleted",
).transition_lifecycle(LifecycleState.DELETE_REQUESTED)
decision = profile.decide_asset_visibility(asset, _context())
assert decision.allowed is False
assert decision.reason == "cmis_lifecycle_not_visible"

View File

@@ -73,7 +73,8 @@ def test_mapper_exposes_repository_info_capabilities_and_base_type_definitions()
assert repository["cmis_version_supported"] == "1.1"
assert repository["binding"] == "browser"
assert repository["capabilities"]["capability_query"] == "metadataonly"
assert repository["capabilities"]["capability_multifiling"] is True
assert repository["capabilities"]["capability_multifiling"] is False
assert repository["unsupported_features"]["multifiling"]["status"] == "projection_only"
assert set(types) == {
"cmis:document",
@@ -122,6 +123,8 @@ def test_mapper_projects_asset_to_cmis_document_envelope() -> None:
assert serialized["properties"]["cmis:objectTypeId"] == "kontextual:document"
assert serialized["properties"]["kontextual:metadata:status"] == "accepted"
assert serialized["content_stream"]["mime_type"] == "text/markdown"
assert serialized["properties"]["cmis:contentStreamMimeType"] == "text/markdown"
assert serialized["properties"]["cmis:contentStreamId"] == "repr-source"
assert serialized["version"]["cmis:versionLabel"] == "3"
assert serialized["relationships"] == ["cmis:relationship:rel-derived"]
assert CMISAction.GET_CONTENT_STREAM.value in serialized["allowable_actions"]

View File

@@ -91,7 +91,9 @@ def test_runtime_cmis_browser_repository_types_children_and_object(cmis_runtime)
assert access_points["count"] == 4
assert repository["repository_id"] == "kontextual-readonly-browser"
assert repository["capabilities"]["capability_get_descendants"] is True
assert repository["capabilities"]["capability_get_descendants"] is False
assert repository["capabilities"]["capability_get_folder_tree"] is False
assert repository["unsupported_features"]["get_descendants"]["status"] == "unsupported"
assert {item["base_type_id"] for item in types["items"]} >= {"cmis:document", "cmis:folder"}
root_paths = {item["path"] for item in children["objects"]}
topic_object_ids = {item["object_id"] for item in topic_children["objects"]}
@@ -183,6 +185,23 @@ def test_runtime_cmis_governed_authoring_allows_selected_mutations(cmis_runtime)
assert stream_bytes.representation.storage_ref.startswith("blob://memory/")
assert deleted["deleted"] is False
assert deleted["lifecycle"] == "delete_requested"
with pytest.raises(Exception) as exc_info:
runtime.cmis_object("governed-authoring", "cmis:asset:asset-authored", context)
assert "CMIS object not found" in str(exc_info.value)
def test_runtime_cmis_rejects_unsupported_standard_property_updates(cmis_runtime) -> None:
runtime, context = cmis_runtime
with pytest.raises(Exception) as exc_info:
runtime.cmis_update_properties(
"governed-authoring",
"cmis:asset:asset-runtime-source",
{"properties": {"cmis:name": "Renamed"}},
context,
)
assert "Unsupported CMIS property update" in str(exc_info.value)
def test_runtime_cmis_readonly_profile_rejects_mutations(cmis_runtime) -> None: