Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,247 @@
|
||||
"""Unit tests for the document read/retire use cases a sync client drives:
|
||||
look a document up by its own (source_id, external_id) identity, list every
|
||||
version, and soft-retire a document that's disappeared at the source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.application.authorization import AuthorizationService
|
||||
from ragcore.application.control.api import ControlPlaneApplication
|
||||
from ragcore.application.documents import (
|
||||
DocumentRecord,
|
||||
DocumentVersionRecord,
|
||||
DocumentVersionState,
|
||||
)
|
||||
from ragcore.domain.authorization import AuthorizationError, Principal, PrincipalType
|
||||
from ragcore.domain.control import Role
|
||||
|
||||
|
||||
class FakeDocumentRepository:
|
||||
def __init__(self) -> None:
|
||||
self.documents: dict[object, DocumentRecord] = {}
|
||||
self.versions: dict[object, list[DocumentVersionRecord]] = {}
|
||||
self.retired: list[object] = []
|
||||
|
||||
def seed(self, document: DocumentRecord, *versions: DocumentVersionRecord) -> None:
|
||||
self.documents[document.id] = document
|
||||
self.versions[document.id] = list(versions)
|
||||
|
||||
async def get_document(self, document_id: object) -> DocumentRecord | None:
|
||||
return self.documents.get(document_id)
|
||||
|
||||
async def get_document_by_source(
|
||||
self, workspace_id: object, source_key: str, external_id: str
|
||||
) -> DocumentRecord | None:
|
||||
return next(
|
||||
(
|
||||
document
|
||||
for document in self.documents.values()
|
||||
if document.workspace_id == workspace_id
|
||||
and document.source_key == source_key
|
||||
and document.external_id == external_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
async def list_versions(self, document_id: object) -> tuple[DocumentVersionRecord, ...]:
|
||||
return tuple(self.versions.get(document_id, ()))
|
||||
|
||||
async def list_searchable_versions(
|
||||
self, document_id: object
|
||||
) -> tuple[DocumentVersionRecord, ...]:
|
||||
document = self.documents[document_id]
|
||||
if document.source_deleted:
|
||||
return ()
|
||||
return tuple(
|
||||
version
|
||||
for version in self.versions.get(document_id, ())
|
||||
if version.state is DocumentVersionState.PUBLISHED
|
||||
)
|
||||
|
||||
async def mark_source_deleted(self, document_id: object) -> None:
|
||||
self.retired.append(document_id)
|
||||
document = self.documents[document_id]
|
||||
self.documents[document_id] = DocumentRecord(
|
||||
id=document.id,
|
||||
workspace_id=document.workspace_id,
|
||||
space_id=document.space_id,
|
||||
source_key=document.source_key,
|
||||
external_id=document.external_id,
|
||||
title=document.title,
|
||||
source_deleted=True,
|
||||
)
|
||||
|
||||
|
||||
def _principal(workspace_id: object, role: Role = Role.EDITOR) -> Principal:
|
||||
return Principal(uuid4(), workspace_id, PrincipalType.SERVICE_ACCOUNT, frozenset({role}))
|
||||
|
||||
|
||||
def _document(workspace_id: object, *, source_deleted: bool = False) -> DocumentRecord:
|
||||
return DocumentRecord(
|
||||
id=uuid4(),
|
||||
workspace_id=workspace_id,
|
||||
space_id=uuid4(),
|
||||
source_key=str(uuid4()),
|
||||
external_id="wiki-page-1",
|
||||
title="Wiki page",
|
||||
source_deleted=source_deleted,
|
||||
)
|
||||
|
||||
|
||||
def _version(document_id: object, state: DocumentVersionState) -> DocumentVersionRecord:
|
||||
return DocumentVersionRecord(
|
||||
id=uuid4(),
|
||||
document_id=document_id,
|
||||
source_fingerprint="a" * 64,
|
||||
content_hash="b" * 64,
|
||||
parser_version="text-v1",
|
||||
chunk_profile_version="prose-v1",
|
||||
state=state,
|
||||
)
|
||||
|
||||
|
||||
def _service(repository: FakeDocumentRepository) -> ControlPlaneApplication:
|
||||
return ControlPlaneApplication(
|
||||
repository=None, # type: ignore[arg-type]
|
||||
authorization=AuthorizationService(),
|
||||
document_repository=repository, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_source_returns_the_active_version() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id)
|
||||
published = _version(document.id, DocumentVersionState.PUBLISHED)
|
||||
repository.seed(document, published)
|
||||
service = _service(repository)
|
||||
|
||||
found, active = await service.get_document_by_source(
|
||||
_principal(workspace_id),
|
||||
source_id=UUID(document.source_key),
|
||||
external_id=document.external_id,
|
||||
)
|
||||
|
||||
assert found == document
|
||||
assert active == published
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_source_unknown_pair_raises_lookup_error() -> None:
|
||||
service = _service(FakeDocumentRepository())
|
||||
with pytest.raises(LookupError):
|
||||
await service.get_document_by_source(
|
||||
_principal(uuid4()), source_id=uuid4(), external_id="missing"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_source_wrong_workspace_raises_lookup_error() -> None:
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(uuid4())
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
with pytest.raises(LookupError):
|
||||
await service.get_document_by_source(
|
||||
_principal(uuid4()),
|
||||
source_id=UUID(document.source_key),
|
||||
external_id=document.external_id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_source_insufficient_role_raises_authorization_error() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id)
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
with pytest.raises(AuthorizationError):
|
||||
await service.get_document_by_source(
|
||||
_principal(workspace_id, role=Role.AUDITOR),
|
||||
source_id=UUID(document.source_key),
|
||||
external_id=document.external_id,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_document_versions_returns_every_state() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id)
|
||||
superseded = _version(document.id, DocumentVersionState.SUPERSEDED)
|
||||
published = _version(document.id, DocumentVersionState.PUBLISHED)
|
||||
repository.seed(document, superseded, published)
|
||||
service = _service(repository)
|
||||
|
||||
versions = await service.list_document_versions(_principal(workspace_id), document.id)
|
||||
|
||||
assert {version.id for version in versions} == {superseded.id, published.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_document_versions_wrong_workspace_raises_lookup_error() -> None:
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(uuid4())
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
with pytest.raises(LookupError):
|
||||
await service.list_document_versions(_principal(uuid4()), document.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retire_document_marks_source_deleted() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id)
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
retired = await service.retire_document(_principal(workspace_id), document.id)
|
||||
|
||||
assert retired.source_deleted is True
|
||||
assert repository.retired == [document.id]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retire_document_is_idempotent() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id, source_deleted=True)
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
retired = await service.retire_document(_principal(workspace_id), document.id)
|
||||
|
||||
assert retired.source_deleted is True
|
||||
assert repository.retired == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retire_document_wrong_workspace_raises_lookup_error() -> None:
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(uuid4())
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
with pytest.raises(LookupError):
|
||||
await service.retire_document(_principal(uuid4()), document.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retire_document_requires_source_manage_permission() -> None:
|
||||
workspace_id = uuid4()
|
||||
repository = FakeDocumentRepository()
|
||||
document = _document(workspace_id)
|
||||
repository.seed(document)
|
||||
service = _service(repository)
|
||||
|
||||
with pytest.raises(AuthorizationError):
|
||||
await service.retire_document(_principal(workspace_id, role=Role.AUDITOR), document.id)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Unit tests for control-plane space and grant use cases."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.application.control import (
|
||||
EffectiveSpaceResolver,
|
||||
GrantService,
|
||||
KnowledgeSpaceLifecycle,
|
||||
)
|
||||
from ragcore.domain.control import (
|
||||
DataPolicy,
|
||||
ProviderKind,
|
||||
Role,
|
||||
SpaceOperation,
|
||||
Workspace,
|
||||
)
|
||||
|
||||
|
||||
def setup_control_plane() -> tuple[Workspace, object, object, object]:
|
||||
workspace = Workspace(uuid4(), "Platform", "platform")
|
||||
application = workspace.create_application(
|
||||
id=uuid4(),
|
||||
name="Client",
|
||||
scopes=(),
|
||||
default_knowledge_space_ids=frozenset(),
|
||||
)
|
||||
private = workspace.create_knowledge_space(
|
||||
id=uuid4(), name="Private", slug="private", data_policy=DataPolicy.LOCAL_ONLY
|
||||
)
|
||||
shared = workspace.create_knowledge_space(
|
||||
id=uuid4(), name="Shared", slug="shared", data_policy=DataPolicy.LOCAL_PREFERRED
|
||||
)
|
||||
return workspace, application, private, shared
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("policy", "provider", "operation", "allowed"),
|
||||
[
|
||||
(DataPolicy.LOCAL_ONLY, ProviderKind.LOCAL, SpaceOperation.ANSWER, True),
|
||||
(DataPolicy.LOCAL_ONLY, ProviderKind.CLOUD, SpaceOperation.SEARCH, False),
|
||||
(DataPolicy.LOCAL_PREFERRED, ProviderKind.CLOUD, SpaceOperation.ANSWER, True),
|
||||
(DataPolicy.RETRIEVAL_ONLY, ProviderKind.LOCAL, SpaceOperation.SEARCH, True),
|
||||
(DataPolicy.NO_GENERATION, ProviderKind.CLOUD, SpaceOperation.CONTEXT, True),
|
||||
(DataPolicy.NO_GENERATION, ProviderKind.LOCAL, SpaceOperation.ANSWER, False),
|
||||
],
|
||||
)
|
||||
def test_data_policy_excludes_incompatible_provider_operations(
|
||||
policy: DataPolicy, provider: ProviderKind, operation: SpaceOperation, allowed: bool
|
||||
) -> None:
|
||||
workspace = Workspace(uuid4(), "Platform", "platform")
|
||||
space = workspace.create_knowledge_space(
|
||||
id=uuid4(), name="Policy", slug="policy", data_policy=policy
|
||||
)
|
||||
|
||||
assert space.allows(operation=operation, provider=provider) is allowed
|
||||
|
||||
|
||||
def test_effective_spaces_are_requested_grants_and_policy_intersection() -> None:
|
||||
workspace, application, private, shared = setup_control_plane()
|
||||
user_id = uuid4()
|
||||
grants = GrantService()
|
||||
app_grant = grants.application_grant(
|
||||
workspace, id=uuid4(), application=application, space=private, role=Role.VIEWER
|
||||
)
|
||||
user_grant = grants.user_grant(
|
||||
workspace, id=uuid4(), user_id=user_id, space=shared, role=Role.VIEWER
|
||||
)
|
||||
|
||||
effective = EffectiveSpaceResolver().resolve(
|
||||
workspace_id=workspace.id,
|
||||
spaces=(private, shared),
|
||||
grants=(app_grant,),
|
||||
user_grants=(user_grant,),
|
||||
application=application,
|
||||
user_id=user_id,
|
||||
requested_space_ids=(private.id, shared.id),
|
||||
)
|
||||
|
||||
assert effective == frozenset({private.id, shared.id})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("role", "operation"),
|
||||
[
|
||||
(Role.VIEWER, SpaceOperation.CONTEXT),
|
||||
(Role.VIEWER, SpaceOperation.ANSWER),
|
||||
(Role.AUDITOR, SpaceOperation.SEARCH),
|
||||
],
|
||||
)
|
||||
def test_grant_role_limits_effective_space_operations(
|
||||
role: Role, operation: SpaceOperation
|
||||
) -> None:
|
||||
workspace, application, private, _ = setup_control_plane()
|
||||
grant = GrantService().application_grant(
|
||||
workspace, id=uuid4(), application=application, space=private, role=role
|
||||
)
|
||||
|
||||
effective = EffectiveSpaceResolver().resolve(
|
||||
workspace_id=workspace.id,
|
||||
spaces=(private,),
|
||||
grants=(grant,),
|
||||
application=application,
|
||||
requested_space_ids=(private.id,),
|
||||
operation=operation,
|
||||
)
|
||||
|
||||
assert effective == frozenset()
|
||||
def test_default_spaces_never_expand_grants() -> None:
|
||||
workspace, application, private, shared = setup_control_plane()
|
||||
application = workspace.create_application(
|
||||
id=application.id,
|
||||
name=application.name,
|
||||
scopes=(),
|
||||
default_knowledge_space_ids=frozenset({private.id, shared.id}),
|
||||
)
|
||||
grant = GrantService().application_grant(
|
||||
workspace, id=uuid4(), application=application, space=private, role=Role.VIEWER
|
||||
)
|
||||
|
||||
effective = EffectiveSpaceResolver().resolve(
|
||||
workspace_id=workspace.id,
|
||||
spaces=(private, shared),
|
||||
grants=(grant,),
|
||||
application=application,
|
||||
)
|
||||
|
||||
assert effective == frozenset({private.id})
|
||||
|
||||
|
||||
def test_inactive_spaces_and_grants_are_excluded() -> None:
|
||||
workspace, application, private, shared = setup_control_plane()
|
||||
inactive_space = workspace.create_knowledge_space(
|
||||
id=uuid4(), name="Inactive", slug="inactive", active=False
|
||||
)
|
||||
grants = GrantService()
|
||||
private_grant = grants.application_grant(
|
||||
workspace,
|
||||
id=uuid4(),
|
||||
application=application,
|
||||
space=private,
|
||||
role=Role.VIEWER,
|
||||
active=False,
|
||||
)
|
||||
inactive_grant = grants.application_grant(
|
||||
workspace, id=uuid4(), application=application, space=inactive_space, role=Role.VIEWER
|
||||
)
|
||||
|
||||
effective = EffectiveSpaceResolver().resolve(
|
||||
workspace_id=workspace.id,
|
||||
spaces=(private, shared, inactive_space),
|
||||
grants=(private_grant, inactive_grant),
|
||||
application=application,
|
||||
requested_space_ids=(private.id, inactive_space.id),
|
||||
)
|
||||
|
||||
assert effective == frozenset()
|
||||
|
||||
|
||||
def test_cross_workspace_spaces_and_grants_are_excluded() -> None:
|
||||
workspace, application, private, _ = setup_control_plane()
|
||||
other = Workspace(uuid4(), "Other", "other")
|
||||
other_space = other.create_knowledge_space(id=uuid4(), name="Other", slug="other")
|
||||
cross_grant = other.grant_user(
|
||||
id=uuid4(), user_id=uuid4(), knowledge_space=other_space, role=Role.VIEWER
|
||||
)
|
||||
|
||||
effective = EffectiveSpaceResolver().resolve(
|
||||
workspace_id=workspace.id,
|
||||
spaces=(private, other_space),
|
||||
user_grants=(cross_grant,),
|
||||
application=application,
|
||||
user_id=cross_grant.user_id,
|
||||
requested_space_ids=(private.id, other_space.id),
|
||||
)
|
||||
|
||||
assert effective == frozenset()
|
||||
|
||||
|
||||
def test_lifecycle_transitions_preserve_identity_without_mutating_source() -> None:
|
||||
_, _, private, _ = setup_control_plane()
|
||||
lifecycle = KnowledgeSpaceLifecycle()
|
||||
|
||||
inactive = lifecycle.deactivate(private)
|
||||
cloud = lifecycle.set_data_policy(inactive, DataPolicy.CLOUD_ALLOWED)
|
||||
active = lifecycle.activate(cloud)
|
||||
|
||||
assert private.active is True
|
||||
assert inactive.active is False
|
||||
assert cloud.data_policy is DataPolicy.CLOUD_ALLOWED
|
||||
assert active.active is True
|
||||
assert active.id == private.id
|
||||
|
||||
|
||||
Reference in new issue
Block a user