Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:21 +02:00
commit 7ca726caf2
659 files changed
+98171

No files matched your search

@@ -0,0 +1,352 @@
"""BackupService orchestration against fake ports: atomicity, verification, retention."""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from ragcore.application.backup.models import (
ArtifactChecksum,
BackupManifest,
BackupStatus,
BackupVerificationError,
)
from ragcore.application.backup.service import MANIFEST_FILENAME, BackupService
def _write_and_checksum(path: Path, content: bytes) -> ArtifactChecksum:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(content)
return ArtifactChecksum(
relative_path=path.name, sha256=hashlib.sha256(content).hexdigest(), size_bytes=len(content)
)
class _FakePostgresDumper:
def __init__(self, *, content: bytes = b"pg-dump-bytes", revision: str = "abc123") -> None:
self._content = content
self._revision = revision
self.dump_calls = 0
async def dump(self, *, destination: Path) -> ArtifactChecksum:
self.dump_calls += 1
return _write_and_checksum(destination, self._content)
async def schema_revision(self) -> str:
return self._revision
class _FakeQdrantSnapshotter:
def __init__(
self,
*,
collections: Mapping[str, bytes] | None = None,
aliases: Mapping[str, str] | None = None,
) -> None:
self._collections = collections if collections is not None else {"docs": b"snapshot-bytes"}
self._aliases = aliases if aliases is not None else {"docs-live": "docs"}
async def snapshot_collections(
self, *, collection_names: Sequence[str], destination_dir: Path
) -> tuple[ArtifactChecksum, ...]:
checksums = []
for name in collection_names:
content = self._collections[name]
checksums.append(_write_and_checksum(destination_dir / f"{name}.snapshot", content))
return tuple(checksums)
async def collection_aliases(self) -> Mapping[str, str]:
return self._aliases
class _FakeFileCopier:
def __init__(self, *, files: Mapping[str, bytes] | None = None) -> None:
self._files = files if files is not None else {"a.bin": b"blob-a", "sub/b.bin": b"blob-b"}
async def copy_tree(self, *, source: Path, destination: Path) -> tuple[ArtifactChecksum, ...]:
checksums = []
for relative_path, content in self._files.items():
target = destination / relative_path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(content)
checksums.append(
ArtifactChecksum(
relative_path=relative_path,
sha256=hashlib.sha256(content).hexdigest(),
size_bytes=len(content),
)
)
return tuple(checksums)
def _service(
*,
postgres: _FakePostgresDumper | None = None,
qdrant: _FakeQdrantSnapshotter | None = None,
files: _FakeFileCopier | None = None,
clock: object = None,
id_factory: object = None,
) -> BackupService:
kwargs: dict[str, object] = {
"postgres": postgres or _FakePostgresDumper(),
"qdrant": qdrant or _FakeQdrantSnapshotter(),
"files": files or _FakeFileCopier(),
}
if clock is not None:
kwargs["clock"] = clock
if id_factory is not None:
kwargs["id_factory"] = id_factory
return BackupService(**kwargs) # type: ignore[arg-type]
async def test_run_produces_a_complete_manifest_pinning_schema_and_profiles(tmp_path: Path) -> None:
service = _service(id_factory=lambda: "backup-1")
manifest = await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={"embedding": "hybrid-v1:v3", "generation": "default:v1"},
)
assert manifest.status is BackupStatus.COMPLETE
assert manifest.backup_id == "backup-1"
assert manifest.schema_revision == "abc123"
assert manifest.active_profiles == {"embedding": "hybrid-v1:v3", "generation": "default:v1"}
assert manifest.postgres_dump is not None
assert manifest.postgres_dump.relative_path == "postgres.dump"
assert len(manifest.qdrant_snapshots) == 1
assert manifest.qdrant_snapshots[0].relative_path == "qdrant/docs.snapshot"
assert manifest.collection_aliases == {"docs-live": "docs"}
assert {c.relative_path for c in manifest.blob_artifacts} == {"blobs/a.bin", "blobs/sub/b.bin"}
assert {c.relative_path for c in manifest.parsed_artifacts} == {
"parsed/a.bin",
"parsed/sub/b.bin",
}
assert manifest.counts == {"qdrant_snapshots": 1, "blob_artifacts": 2, "parsed_artifacts": 2}
async def test_run_filters_out_aliases_for_collections_not_in_this_backup(tmp_path: Path) -> None:
"""collection_aliases() reports every alias in the whole Qdrant cluster;
only the ones for collections this backup actually snapshotted may end
up in the manifest, or restore would later look for a snapshot of
someone else's unrelated collection."""
service = _service(
qdrant=_FakeQdrantSnapshotter(
collections={"docs": b"snapshot-bytes"},
aliases={"docs-live": "docs", "unrelated-live": "unrelated-collection"},
),
id_factory=lambda: "backup-1",
)
manifest = await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
assert manifest.collection_aliases == {"docs-live": "docs"}
async def test_run_writes_every_artifact_relative_to_backup_dir_on_disk(tmp_path: Path) -> None:
service = _service(id_factory=lambda: "backup-1")
manifest = await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
backup_dir = tmp_path / "backup-1"
assert manifest.postgres_dump is not None
for checksum in (
manifest.postgres_dump,
*manifest.qdrant_snapshots,
*manifest.blob_artifacts,
*manifest.parsed_artifacts,
):
assert (backup_dir / checksum.relative_path).is_file()
async def test_manifest_on_disk_is_incomplete_before_run_finishes(tmp_path: Path) -> None:
"""The atomic-completion invariant: an in-flight backup must never look done."""
seen_status: list[str] = []
real_dump = _FakePostgresDumper()
class _ObservingDumper(_FakePostgresDumper):
async def dump(self, *, destination: Path) -> ArtifactChecksum:
manifest_path = destination.parent / MANIFEST_FILENAME
seen_status.append(json.loads(manifest_path.read_text())["status"])
return await real_dump.dump(destination=destination)
async def schema_revision(self) -> str:
return await real_dump.schema_revision()
service = _service(postgres=_ObservingDumper(), id_factory=lambda: "backup-2")
await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
assert seen_status == ["incomplete"]
final_status = json.loads((tmp_path / "backup-2" / MANIFEST_FILENAME).read_text())["status"]
assert final_status == "complete"
async def test_a_partial_backup_is_never_promoted_to_complete(tmp_path: Path) -> None:
class _FailingFileCopier(_FakeFileCopier):
async def copy_tree(
self, *, source: Path, destination: Path
) -> tuple[ArtifactChecksum, ...]:
raise RuntimeError("disk full")
service = _service(files=_FailingFileCopier(), id_factory=lambda: "backup-3")
with pytest.raises(RuntimeError, match="disk full"):
await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
manifest_path = tmp_path / "backup-3" / MANIFEST_FILENAME
assert json.loads(manifest_path.read_text())["status"] == "incomplete"
async def test_run_raises_if_a_checksum_is_corrupted_before_verification(tmp_path: Path) -> None:
"""Simulates on-disk corruption between write and the re-verify step."""
class _CorruptingFileCopier(_FakeFileCopier):
async def copy_tree(
self, *, source: Path, destination: Path
) -> tuple[ArtifactChecksum, ...]:
checksums = await super().copy_tree(source=source, destination=destination)
first = destination / checksums[0].relative_path
first.write_bytes(b"corrupted")
return checksums
service = _service(files=_CorruptingFileCopier(), id_factory=lambda: "backup-4")
with pytest.raises(BackupVerificationError, match="mismatch"):
await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
manifest_path = tmp_path / "backup-4" / MANIFEST_FILENAME
assert json.loads(manifest_path.read_text())["status"] == "incomplete"
async def test_run_raises_if_an_artifact_is_missing_before_verification(tmp_path: Path) -> None:
class _DeletingFileCopier(_FakeFileCopier):
async def copy_tree(
self, *, source: Path, destination: Path
) -> tuple[ArtifactChecksum, ...]:
checksums = await super().copy_tree(source=source, destination=destination)
(destination / checksums[0].relative_path).unlink()
return checksums
service = _service(files=_DeletingFileCopier(), id_factory=lambda: "backup-5")
with pytest.raises(BackupVerificationError, match="missing"):
await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={},
)
async def test_manifest_never_contains_settings_style_secret_fields(tmp_path: Path) -> None:
service = _service(id_factory=lambda: "backup-6")
manifest = await service.run(
backup_root=tmp_path,
blob_root=tmp_path / "blobs-src",
parsed_root=tmp_path / "parsed-src",
collection_names=["docs"],
active_profiles={"embedding": "hybrid-v1:v3"},
)
serialized = json.dumps(manifest.to_json_dict())
for forbidden in ("database_url", "api_key", "secret", "password", "token"):
assert forbidden not in serialized.lower()
def _manifest(backup_id: str, status: BackupStatus, created_at: datetime) -> BackupManifest:
return BackupManifest(
backup_id=backup_id,
status=status,
created_at=created_at,
software_version="0.0.0",
schema_revision="rev",
)
def _write_manifest(backup_root: Path, manifest: BackupManifest) -> None:
backup_dir = backup_root / manifest.backup_id
backup_dir.mkdir(parents=True)
(backup_dir / MANIFEST_FILENAME).write_text(json.dumps(manifest.to_json_dict()))
def test_prune_keeps_only_the_newest_complete_backups(tmp_path: Path) -> None:
base = datetime(2026, 1, 1, tzinfo=UTC)
_write_manifest(tmp_path, _manifest("oldest", BackupStatus.COMPLETE, base))
_write_manifest(tmp_path, _manifest("middle", BackupStatus.COMPLETE, base + timedelta(days=1)))
_write_manifest(tmp_path, _manifest("newest", BackupStatus.COMPLETE, base + timedelta(days=2)))
service = _service()
removed = service.prune(backup_root=tmp_path, keep_complete=2)
assert removed == ("oldest",)
assert not (tmp_path / "oldest").exists()
assert (tmp_path / "middle").exists()
assert (tmp_path / "newest").exists()
def test_prune_never_removes_an_incomplete_backup(tmp_path: Path) -> None:
base = datetime(2026, 1, 1, tzinfo=UTC)
_write_manifest(tmp_path, _manifest("stale-incomplete", BackupStatus.INCOMPLETE, base))
_write_manifest(
tmp_path, _manifest("complete-one", BackupStatus.COMPLETE, base + timedelta(days=1))
)
service = _service()
removed = service.prune(backup_root=tmp_path, keep_complete=0)
assert removed == ("complete-one",)
assert (tmp_path / "stale-incomplete").exists()
def test_prune_rejects_negative_retention(tmp_path: Path) -> None:
service = _service()
with pytest.raises(ValueError, match="keep_complete"):
service.prune(backup_root=tmp_path, keep_complete=-1)
def test_prune_on_nonexistent_backup_root_is_a_noop(tmp_path: Path) -> None:
service = _service()
removed = service.prune(backup_root=tmp_path / "does-not-exist", keep_complete=5)
assert removed == ()
@@ -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
@@ -0,0 +1,171 @@
"""Batch byte-quota boundaries: exact limit, one byte over, and fail-closed I/O."""
from __future__ import annotations
from dataclasses import replace
from types import SimpleNamespace
from typing import Any
from uuid import uuid4
import pytest
from ragcore.application.indexes.models import point_wire_bytes
from ragcore.infrastructure.qdrant import adapter as adapter_module
from ragcore.infrastructure.qdrant.adapter import QdrantIndexAdapter
from ragcore.infrastructure.qdrant.models import (
CollectionKind,
CollectionProfile,
IndexPayload,
IndexPoint,
)
class _RefusingClient:
"""Proves a quota rejection happens before any network mutation is attempted."""
async def upsert(self, *args: Any, **kwargs: Any) -> None:
raise AssertionError("upsert must not run once the batch quota is exceeded")
async def retrieve(self, *args: Any, **kwargs: Any) -> None:
raise AssertionError("retrieve must not run once the batch quota is exceeded")
async def set_payload(self, *args: Any, **kwargs: Any) -> None:
raise AssertionError("set_payload must not run once the batch quota is exceeded")
class _RecordingClient:
"""A minimal working double so an at-quota batch can complete normally."""
def __init__(self) -> None:
self.upserted_ids: list[str] = []
async def upsert(self, *, points: Any, **kwargs: Any) -> None:
self.upserted_ids = [point.id for point in points]
async def retrieve(self, *, ids: list[str], **kwargs: Any) -> list[SimpleNamespace]:
return [
SimpleNamespace(
id=point_id,
vector={
"dense": [0.1, 0.2, 0.3, 0.4],
"sparse": SimpleNamespace(indices=[1], values=[0.5]),
},
)
for point_id in ids
]
async def set_payload(self, **kwargs: Any) -> None:
return None
def _profile() -> CollectionProfile:
return CollectionProfile(
kind=CollectionKind.DENSE,
profile="quota",
version="v1",
dimensions=4,
model_digest="a" * 64,
)
def _point(profile: CollectionProfile, *, text: str = "x" * 1000) -> IndexPoint:
chunk_id = uuid4()
return IndexPoint(
point_id=chunk_id,
dense_vector=(0.1, 0.2, 0.3, 0.4),
sparse_indices=(1, 2),
sparse_values=(0.5, 0.5),
payload=IndexPayload(
workspace_id=uuid4(),
space_id=uuid4(),
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=chunk_id,
parent_chunk_id=None,
source_id=uuid4(),
status="published",
content_type="text/markdown",
language="en",
trust_level="source",
location={"page": 1},
parser_version="text-v1",
chunk_profile_version=profile.version,
embedding_model_digest=profile.model_digest,
text=text,
excerpt="short excerpt",
),
)
async def test_batch_exactly_at_the_byte_limit_is_accepted(monkeypatch: pytest.MonkeyPatch) -> None:
profile = _profile()
point = _point(profile)
monkeypatch.setattr(adapter_module, "MAX_BATCH_BYTES", point_wire_bytes(point))
adapter = QdrantIndexAdapter(_RecordingClient()) # type: ignore[arg-type]
await adapter.upsert_points(profile, (point,))
async def test_batch_one_byte_over_the_limit_is_rejected_with_no_partial_mutation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
point = _point(profile)
monkeypatch.setattr(adapter_module, "MAX_BATCH_BYTES", point_wire_bytes(point) - 1)
adapter = QdrantIndexAdapter(_RefusingClient()) # type: ignore[arg-type]
with pytest.raises(ValueError, match="serialized batch limit"):
await adapter.upsert_points(profile, (point,))
async def test_batch_quota_counts_every_metadata_field_not_only_text(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
small = _point(profile, text="short")
large_location = replace(
small.payload, location={f"k{i}": f"v{i}" for i in range(500)}
)
large_metadata_point = replace(small, payload=large_location)
# Set the limit strictly between the two sizes: only the metadata-heavy point
# must be rejected, proving the estimate is not text/excerpt-only.
threshold = (point_wire_bytes(small) + point_wire_bytes(large_metadata_point)) // 2
monkeypatch.setattr(adapter_module, "MAX_BATCH_BYTES", threshold)
accepting_adapter = QdrantIndexAdapter(_RecordingClient()) # type: ignore[arg-type]
await accepting_adapter.upsert_points(profile, (small,))
refusing_adapter = QdrantIndexAdapter(_RefusingClient()) # type: ignore[arg-type]
with pytest.raises(ValueError, match="serialized batch limit"):
await refusing_adapter.upsert_points(profile, (large_metadata_point,))
async def test_batch_quota_counts_multibyte_unicode_by_real_utf8_bytes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
ascii_point = _point(profile, text="a" * 300)
multibyte_point = _point(profile, text="漢" * 300)
assert point_wire_bytes(multibyte_point) > point_wire_bytes(ascii_point)
monkeypatch.setattr(adapter_module, "MAX_BATCH_BYTES", point_wire_bytes(ascii_point))
refusing_adapter = QdrantIndexAdapter(_RefusingClient()) # type: ignore[arg-type]
with pytest.raises(ValueError, match="serialized batch limit"):
await refusing_adapter.upsert_points(profile, (multibyte_point,))
async def test_batch_quota_is_cumulative_across_many_small_points(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
points = tuple(_point(profile, text=f"passage {i}") for i in range(20))
per_point = point_wire_bytes(points[0])
# Allow 5 points' worth of bytes; the 6th must trip the cumulative check even
# though no single point is anywhere near the limit on its own.
monkeypatch.setattr(adapter_module, "MAX_BATCH_BYTES", per_point * 5 + per_point // 2)
refusing_adapter = QdrantIndexAdapter(_RefusingClient()) # type: ignore[arg-type]
with pytest.raises(ValueError, match="serialized batch limit"):
await refusing_adapter.upsert_points(profile, points)
@@ -0,0 +1,109 @@
"""Cumulative per-build byte quota: many small points must not evade it."""
from __future__ import annotations
from collections.abc import AsyncIterator
from uuid import uuid4
import pytest
import ragcore.application.indexes.service as service_module
from ragcore.application.indexes import models as models_module
from ragcore.application.indexes.models import IndexBuildError
from ragcore.application.indexes.service import _stream_build
from ragcore.infrastructure.qdrant.models import (
CollectionKind,
CollectionProfile,
IndexPayload,
IndexPoint,
)
def _profile() -> CollectionProfile:
return CollectionProfile(
kind=CollectionKind.DENSE,
profile="build-quota",
version="v1",
dimensions=4,
model_digest="a" * 64,
)
def _point(profile: CollectionProfile, index: int) -> IndexPoint:
chunk_id = uuid4()
return IndexPoint(
point_id=chunk_id,
dense_vector=(0.1, 0.2, 0.3, 0.4),
sparse_indices=(),
sparse_values=(),
payload=IndexPayload(
workspace_id=uuid4(),
space_id=uuid4(),
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=chunk_id,
parent_chunk_id=None,
source_id=uuid4(),
status="published",
content_type="text/markdown",
language="en",
trust_level="source",
location={"page": index},
parser_version="text-v1",
chunk_profile_version=profile.version,
embedding_model_digest=profile.model_digest,
text=f"passage {index}",
excerpt=f"passage {index}",
),
)
class _RecordingStore:
def __init__(self) -> None:
self.upserted: list[IndexPoint] = []
async def upsert_points(self, profile: CollectionProfile, points: object) -> None:
self.upserted.extend(points) # type: ignore[arg-type]
class _ListSource:
def __init__(self, points: tuple[IndexPoint, ...]) -> None:
self._points = points
async def iter_points(self, profile: CollectionProfile) -> AsyncIterator[IndexPoint]:
for point in self._points:
yield point
async def test_many_small_points_are_bounded_by_the_cumulative_build_quota(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
points = tuple(_point(profile, index) for index in range(10))
per_point_bytes = models_module.point_wire_bytes(points[0])
# _stream_build resolves MAX_BUILD_BYTES against service.py's own module
# globals (bound at import time), so it must be patched there, not on models.
monkeypatch.setattr(
service_module, "MAX_BUILD_BYTES", per_point_bytes * 5 + per_point_bytes // 2
)
store = _RecordingStore()
with pytest.raises(IndexBuildError, match="byte quota"):
await _stream_build(store, profile, _ListSource(points))
# Fewer than all ten points may have been accepted; none beyond the quota.
assert len(store.upserted) < len(points)
async def test_build_quota_is_not_evaded_by_many_tiny_points(
monkeypatch: pytest.MonkeyPatch,
) -> None:
profile = _profile()
points = tuple(_point(profile, index) for index in range(50))
per_point_bytes = models_module.point_wire_bytes(points[0])
monkeypatch.setattr(service_module, "MAX_BUILD_BYTES", per_point_bytes * 10)
store = _RecordingStore()
with pytest.raises(IndexBuildError, match="byte quota"):
await _stream_build(store, profile, _ListSource(points))
@@ -0,0 +1,101 @@
"""Pure unit coverage for the point_wire_bytes byte-quoting estimator."""
from __future__ import annotations
from dataclasses import replace
from uuid import uuid4
from ragcore.application.indexes.models import point_wire_bytes
from ragcore.infrastructure.qdrant.models import (
CollectionKind,
CollectionProfile,
IndexPayload,
IndexPoint,
)
def _profile() -> CollectionProfile:
return CollectionProfile(
kind=CollectionKind.DENSE,
profile="quota-test",
version="v1",
dimensions=4,
model_digest="a" * 64,
)
def _point(profile: CollectionProfile, *, text: str = "safe passage") -> IndexPoint:
chunk_id = uuid4()
return IndexPoint(
point_id=chunk_id,
dense_vector=(0.1, 0.2, 0.3, 0.4),
sparse_indices=(1, 2),
sparse_values=(0.5, 0.5),
payload=IndexPayload(
workspace_id=uuid4(),
space_id=uuid4(),
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=chunk_id,
parent_chunk_id=None,
source_id=uuid4(),
status="published",
content_type="text/markdown",
language="en",
trust_level="source",
location={"page": 1, "block": "heading"},
parser_version="text-v1",
chunk_profile_version=profile.version,
embedding_model_digest=profile.model_digest,
text=text,
excerpt=text,
),
)
def test_wire_bytes_grows_with_non_text_metadata_fields() -> None:
profile = _profile()
small = _point(profile)
large_location = replace(
small.payload, location={f"key_{index}": f"value_{index}" for index in range(200)}
)
large = replace(small, payload=large_location)
assert point_wire_bytes(large) > point_wire_bytes(small)
def test_wire_bytes_grows_with_sparse_vector_size() -> None:
profile = _profile()
small = _point(profile)
large = replace(
small,
sparse_indices=tuple(range(500)),
sparse_values=tuple(0.1 for _ in range(500)),
)
assert point_wire_bytes(large) > point_wire_bytes(small)
def test_wire_bytes_counts_real_utf8_bytes_for_multibyte_text() -> None:
profile = _profile()
ascii_point = _point(profile, text="a" * 100)
multibyte_point = _point(profile, text="漢" * 100) # each character is 3 UTF-8 bytes
assert point_wire_bytes(multibyte_point) > point_wire_bytes(ascii_point) + 150
def test_wire_bytes_includes_fingerprint_field_overhead() -> None:
import json
profile = _profile()
point = _point(profile)
raw_payload_bytes = len(
json.dumps(dict(point.payload.as_dict()), sort_keys=True, separators=(",", ":")).encode(
"utf-8"
)
)
# The adapter attaches three 64-character fingerprint fields after the payload is
# built; the estimate must already include their size so quota cannot be evaded
# by omitting them.
assert point_wire_bytes(point) > raw_payload_bytes + 3 * 64
@@ -0,0 +1,265 @@
"""Incremental ingest and authoritative rebuild must agree on every logical
payload field -- the one canonical ``IndexableChunk`` mapping, exercised from
both ends.
The handler indexes a document (fake Qdrant store records the points, fake
repository records the chunk rows exactly as PostgreSQL would store them).
The rebuild source then reads those *stored* chunk rows back as
``PublishedChunkRecord``s -- the same shape ``iter_published_content`` yields --
and re-embeds them. Apart from the embedding vector (a different profile
would produce a different one) every payload field must be identical: source
identity, document/version/chunk identity, language, content type, trust
level, section, page/line ranges, structure path, parser version.
"""
from __future__ import annotations
import asyncio
import json
from collections.abc import AsyncIterator
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import httpx
import pytest
from ragcore.application.documents import (
ChunkRecord,
DocumentRecord,
DocumentVersionRecord,
DocumentVersionState,
PublishedChunkRecord,
)
from ragcore.application.jobs.models import Job, JobContext
from ragcore.application.sources import SourceRecord
from ragcore.config import Settings
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import EmbeddingProfile, EmbeddingProvider
from ragcore.infrastructure.blob.filesystem import FilesystemBlobStorage
from ragcore.infrastructure.indexes import ProductionAuthoritativeIndexSource
from ragcore.infrastructure.ingestion.handler import IngestUploadHandler
from ragcore.infrastructure.models.embedding_factory import create_embedding_adapter_factory
from ragcore.infrastructure.models.persistence import JsonEmbeddingProfileStore
from ragcore.infrastructure.parsers.publisher import FilesystemArtifactPublisher
from ragcore.infrastructure.qdrant.models import CollectionProfile, IndexPoint
from ragcore.infrastructure.security import (
ContentSecurityGateService,
production_content_security_policy,
)
WORKSPACE_ID = uuid4()
SPACE_ID = uuid4()
SOURCE_ID = uuid4()
DIMENSIONS = 4
class RecordingRepository:
"""Stores documents, versions and chunk rows; replays them as published content."""
def __init__(self) -> None:
self.documents: list[DocumentRecord] = []
self.versions: dict[UUID, DocumentVersionRecord] = {}
self.chunks: dict[UUID, tuple[ChunkRecord, ...]] = {}
async def create_document(self, document: DocumentRecord) -> DocumentRecord:
self.documents.append(document)
return document
async def create_version(self, version: DocumentVersionRecord) -> DocumentVersionRecord:
self.versions[version.id] = version
return version
async def get_version(self, version_id: UUID) -> DocumentVersionRecord | None:
return self.versions.get(version_id)
async def get_document_by_source(
self, workspace_id: UUID, source_key: str, external_id: str
) -> DocumentRecord | None:
return None
async def publish_validated(self, version_id, artifacts, chunks): # type: ignore[no-untyped-def]
self.chunks[version_id] = tuple(chunks)
version = self.versions[version_id]
self.versions[version_id] = DocumentVersionRecord(
id=version.id,
document_id=version.document_id,
source_fingerprint=version.source_fingerprint,
content_hash=version.content_hash,
parser_version=version.parser_version,
chunk_profile_version=version.chunk_profile_version,
state=DocumentVersionState.PUBLISHED,
job_id=version.job_id,
)
return self.versions[version_id]
async def iter_published_content(self) -> AsyncIterator[PublishedChunkRecord]:
for version_id, chunk_rows in self.chunks.items():
version = self.versions[version_id]
document = next(d for d in self.documents if d.id == version.document_id)
for chunk in chunk_rows:
yield PublishedChunkRecord(
workspace_id=document.workspace_id,
space_id=document.space_id,
source_id=document.source_id,
document_id=document.id,
document_version_id=version.id,
chunk_id=chunk.id,
parent_id=chunk.parent_id,
structure_path=chunk.structure_path,
content=chunk.content,
parser_version=version.parser_version,
metadata=dict(chunk.metadata),
)
class RecordingQdrant:
def __init__(self) -> None:
self.points: list[IndexPoint] = []
self.alias_target: str | None = None
async def current_alias_target(self, alias_name: str) -> str | None:
return self.alias_target
async def create_shadow(self, profile: CollectionProfile) -> None:
return None
async def activate(self, profile: CollectionProfile) -> None:
self.alias_target = profile.collection_name
async def upsert_points(self, profile: CollectionProfile, points: list[IndexPoint]) -> None:
self.points.extend(points)
class NullRegistry:
async def ensure_source(self, record: SourceRecord) -> SourceRecord:
return record
class NullAudit:
async def record(self, **event: object) -> None:
return None
def _embed(request: httpx.Request) -> httpx.Response:
body = json.loads(request.content)
return httpx.Response(200, json={"embeddings": [[0.5, 0.5, 0.5, 0.5] for _ in body["input"]]})
async def _profile_store(tmp_path: Path) -> tuple[JsonEmbeddingProfileStore, EmbeddingProfile]:
store = JsonEmbeddingProfileStore(tmp_path / "profiles.json")
profile = EmbeddingProfile(
id=uuid4(),
profile="parity-embed",
version="v1",
provider=EmbeddingProvider.OLLAMA,
model_identifier="parity:v1",
model_digest="d" * 64,
dimensions=DIMENSIONS,
context_limit=2048,
tokenizer="test",
data_policy=DataPolicy.LOCAL_ONLY,
)
await store.add(profile)
await store.activate(profile.id)
return store, profile
async def _bytes(data: bytes) -> AsyncIterator[bytes]:
yield data
@pytest.mark.asyncio
async def test_incremental_ingest_and_authoritative_rebuild_produce_identical_payloads(
tmp_path: Path,
) -> None:
store, profile = await _profile_store(tmp_path)
repository = RecordingRepository()
qdrant = RecordingQdrant()
client = httpx.AsyncClient(transport=httpx.MockTransport(_embed), follow_redirects=False)
adapter_factory = create_embedding_adapter_factory(
settings=Settings(env="test", ollama_url="http://ollama:11434"), http_client=client
)
handler = IngestUploadHandler(
document_repository=repository, # type: ignore[arg-type]
blob_storage=FilesystemBlobStorage(tmp_path / "blobs"),
artifact_publisher=FilesystemArtifactPublisher(tmp_path / "parsed"),
embedding_profile_store=store,
embedding_adapter_factory=adapter_factory,
qdrant_store=qdrant, # type: ignore[arg-type]
source_registry=NullRegistry(),
security_gate=ContentSecurityGateService(
NullAudit(), policy=production_content_security_policy()
),
)
blob = FilesystemBlobStorage(tmp_path / "blobs")
reference = await blob.put(
_bytes(
b"# Backup Runbook\n\n"
b"## Nightly snapshot\n\n"
b"Run the snapshot job SNAP-22 before midnight and verify the checksum.\n\n"
b"## Restore drill\n\n"
b"Restore into the disposable database and compare row counts with production.\n"
),
media_type="text/markdown",
original_name="backup.md",
)
job = Job(
id=uuid4(),
job_key="parity",
operation="ingest_upload",
target_key=reference.digest,
profile_version="upload-v1",
payload={
"blob_digest": reference.digest,
"media_type": "text/markdown",
"original_name": "backup.md",
"space_id": str(SPACE_ID),
"workspace_id": str(WORKSPACE_ID),
"source_id": str(SOURCE_ID),
"external_id": "runbooks/backup.md",
"locale": "en",
"trust_level": "trusted_internal",
},
available_at=datetime.now(UTC),
)
await handler(
JobContext(job=job, attempt_number=1, idempotency_key="k", cancellation=asyncio.Event())
)
ingested = {point.point_id: point for point in qdrant.points}
assert ingested
rebuilt_source = ProductionAuthoritativeIndexSource(
document_repository=repository, # type: ignore[arg-type]
embedding_profile=profile,
embedding_http_client=client,
embedding_base_url="http://ollama:11434",
)
collection = CollectionProfile(
kind="dense", # type: ignore[arg-type]
profile=profile.profile,
version=profile.version,
dimensions=profile.dimensions,
model_digest=profile.model_digest,
)
rebuilt = {point.point_id: point async for point in rebuilt_source.iter_points(collection)}
assert set(rebuilt) == set(ingested), "rebuild must cover exactly the ingested child chunks"
for point_id, original in ingested.items():
again = rebuilt[point_id]
assert isinstance(again, IndexPoint)
assert again.payload.as_dict() == original.payload.as_dict(), point_id
assert again.sparse_indices == original.sparse_indices
assert again.sparse_values == original.sparse_values
# And the provenance that matters is really there, not vacuously equal.
payload = again.payload
assert payload.source_id == SOURCE_ID
assert payload.trust_level == "trusted_internal"
assert payload.language == "en"
assert payload.content_type == "text/markdown"
assert payload.location["structure_path"]
assert isinstance(payload.location.get("line_start"), int)
assert isinstance(payload.location.get("line_end"), int)
sections = {point.payload.location.get("section") for point in rebuilt.values()}
assert sections & {"Nightly snapshot", "Restore drill"}, sections
await client.aclose()
@@ -0,0 +1,152 @@
from __future__ import annotations
from collections.abc import Sequence
from uuid import UUID, uuid4
import pytest
from ragcore.application.documents import (
DocumentRecord,
DocumentVersionRecord,
DocumentVersionState,
)
from ragcore.application.indexes.space_reconcile import (
MAX_DELETE_BATCH,
SpaceIndexReconciler,
SpacePoint,
)
class _Profile:
collection_name = "rag_dense_test_v1"
alias_name = "rag_dense_test_active"
version = "v1"
model_digest = "a" * 64
class FakeDocuments:
def __init__(self) -> None:
self.documents: list[DocumentRecord] = []
self.versions: dict[UUID, list[DocumentVersionRecord]] = {}
def add(self, document: DocumentRecord, *versions: DocumentVersionRecord) -> None:
self.documents.append(document)
self.versions[document.id] = list(versions)
async def list_documents_by_space(self, space_id: UUID) -> tuple[DocumentRecord, ...]:
return tuple(d for d in self.documents if d.space_id == space_id)
async def list_searchable_versions(
self, document_id: UUID
) -> tuple[DocumentVersionRecord, ...]:
document = next(d for d in self.documents if d.id == document_id)
if document.source_deleted:
return ()
return tuple(
v
for v in self.versions.get(document_id, ())
if v.state is DocumentVersionState.PUBLISHED
)
class FakeStore:
def __init__(self, points: dict[UUID, list[SpacePoint]]) -> None:
self.points = points
self.deleted: list[list[UUID]] = []
async def list_space_points(self, profile: object, space_id: UUID) -> tuple[SpacePoint, ...]:
return tuple(self.points.get(space_id, ()))
async def delete_points(self, profile: object, point_ids: Sequence[UUID]) -> None:
self.deleted.append(list(point_ids))
def _document(space_id: UUID, *, retired: bool = False) -> DocumentRecord:
return DocumentRecord(
id=uuid4(),
workspace_id=uuid4(),
space_id=space_id,
source_key=str(uuid4()),
external_id=f"record-{uuid4()}",
title="record",
source_deleted=retired,
)
def _version(document_id: UUID, 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 _points(document: DocumentRecord, version: DocumentVersionRecord, n: int) -> list[SpacePoint]:
return [
SpacePoint(point_id=uuid4(), document_id=document.id, document_version_id=version.id)
for _ in range(n)
]
@pytest.mark.asyncio
async def test_reconcile_drops_superseded_and_retired_points_only_in_the_space() -> None:
space, other_space = uuid4(), uuid4()
documents = FakeDocuments()
current = _document(space)
published = _version(current.id, DocumentVersionState.PUBLISHED)
superseded = _version(current.id, DocumentVersionState.SUPERSEDED)
documents.add(current, published, superseded)
retired = _document(space, retired=True)
retired_version = _version(retired.id, DocumentVersionState.PUBLISHED)
documents.add(retired, retired_version)
foreign = _document(other_space)
foreign_version = _version(foreign.id, DocumentVersionState.PUBLISHED)
documents.add(foreign, foreign_version)
keep = _points(current, published, 3)
stale = _points(current, superseded, 2)
gone = _points(retired, retired_version, 4)
store = FakeStore(
{space: keep + stale + gone, other_space: _points(foreign, foreign_version, 5)}
)
report = await SpaceIndexReconciler(store, _Profile()).reconcile(space, documents) # type: ignore[arg-type]
deleted = {point_id for batch in store.deleted for point_id in batch}
assert deleted == {p.point_id for p in stale + gone}
assert report.points_before == 9
assert report.deleted_points == 6
assert report.points_after == 3
assert report.searchable_versions == 1
assert report.documents_total == 2
assert report.documents_retired == 1
assert report.orphaned_documents == 2
# The other space was never touched.
assert store.points[other_space] and all(
p.point_id not in deleted for p in store.points[other_space]
)
@pytest.mark.asyncio
async def test_reconcile_is_idempotent_and_batches_deletes() -> None:
space = uuid4()
documents = FakeDocuments()
document = _document(space)
old = _version(document.id, DocumentVersionState.SUPERSEDED)
new = _version(document.id, DocumentVersionState.PUBLISHED)
documents.add(document, old, new)
stale = _points(document, old, MAX_DELETE_BATCH + 10)
store = FakeStore({space: stale + _points(document, new, 1)})
first = await SpaceIndexReconciler(store, _Profile()).reconcile(space, documents) # type: ignore[arg-type]
assert first.deleted_points == MAX_DELETE_BATCH + 10
assert [len(batch) for batch in store.deleted] == [MAX_DELETE_BATCH, 10]
store.points[space] = [p for p in store.points[space] if p.document_version_id == new.id]
second = await SpaceIndexReconciler(store, _Profile()).reconcile(space, documents) # type: ignore[arg-type]
assert second.deleted_points == 0
assert second.points_after == 1
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,161 @@
"""JobWorker: ingestion job outcomes and claim age are really recorded."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from uuid import uuid4
import pytest
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.resources import Resource
from ragcore.application.jobs.models import (
Job,
JobClaim,
JobContext,
JobError,
RetryableJobError,
TerminalJobError,
)
from ragcore.application.jobs.worker import JobWorker
from ragcore.platform.telemetry import TelemetryMetrics
def _telemetry() -> tuple[TelemetryMetrics, InMemoryMetricReader]:
reader = InMemoryMetricReader()
provider = MeterProvider(
resource=Resource.create({"service.name": "test"}), metric_readers=[reader]
)
return TelemetryMetrics(meter_provider=provider), reader
def _metric_points(reader: InMemoryMetricReader, name: str) -> list[object]:
data = reader.get_metrics_data()
if data is None:
return []
for resource_metric in data.resource_metrics:
for scope_metric in resource_metric.scope_metrics:
for metric in scope_metric.metrics:
if metric.name == name:
return list(metric.data.data_points)
return []
class _FakeJobRepository:
"""Enough of ``JobRepository`` for one claim/complete or claim/fail cycle."""
def __init__(self, job: Job) -> None:
self._job = job
self._claimed = False
self.completed: list[JobClaim] = []
self.failed: list[tuple[JobClaim, JobError]] = []
async def enqueue(self, job: Job) -> Job:
raise NotImplementedError
async def claim_next(
self, worker_id: str, *, now: datetime, lease_duration: timedelta
) -> JobClaim | None:
if self._claimed:
return None
self._claimed = True
return JobClaim(
job=self._job, worker_id=worker_id, attempt_number=1, lease_until=now + lease_duration
)
async def heartbeat(
self, claim: JobClaim, *, now: datetime, lease_duration: timedelta
) -> bool:
return True
async def complete(self, claim: JobClaim, *, now: datetime) -> None:
self.completed.append(claim)
async def fail(
self,
claim: JobClaim,
error: JobError,
*,
now: datetime,
retry_after: timedelta = timedelta(0),
) -> None:
self.failed.append((claim, error))
def _job(*, available_at: datetime) -> Job:
return Job(
id=uuid4(),
job_key=f"job-{uuid4()}",
operation="ingest_upload",
target_key="target",
profile_version="v1",
available_at=available_at,
)
async def test_successful_job_records_claimed_age_and_succeeded_result() -> None:
telemetry, reader = _telemetry()
available_at = datetime.now(UTC) - timedelta(seconds=5)
repository = _FakeJobRepository(_job(available_at=available_at))
async def handler(ctx: JobContext) -> None:
return None
worker = JobWorker(
repository, {"ingest_upload": handler}, worker_id="worker-1", metrics=telemetry
)
assert await worker.run_once() is True
age_points = _metric_points(reader, "ragcore_job_age_seconds")
assert len(age_points) == 1
assert age_points[0].sum == pytest.approx(5.0, abs=1.0)
result_points = _metric_points(reader, "ragcore_ingestion_job_result_total")
assert len(result_points) == 1
assert dict(result_points[0].attributes) == {
"operation": "ingest_upload",
"result": "succeeded",
}
assert repository.completed
async def test_retryable_failure_records_retryable_failed_result() -> None:
telemetry, reader = _telemetry()
repository = _FakeJobRepository(_job(available_at=datetime.now(UTC)))
async def handler(ctx: JobContext) -> None:
raise RetryableJobError("transient", retry_after_seconds=1.0)
worker = JobWorker(
repository, {"ingest_upload": handler}, worker_id="worker-1", metrics=telemetry
)
assert await worker.run_once() is True
result_points = _metric_points(reader, "ragcore_ingestion_job_result_total")
assert len(result_points) == 1
assert dict(result_points[0].attributes) == {
"operation": "ingest_upload",
"result": "retryable_failed",
}
assert repository.failed
async def test_terminal_failure_records_terminal_failed_result() -> None:
telemetry, reader = _telemetry()
repository = _FakeJobRepository(_job(available_at=datetime.now(UTC)))
async def handler(ctx: JobContext) -> None:
raise TerminalJobError("unrecoverable")
worker = JobWorker(
repository, {"ingest_upload": handler}, worker_id="worker-1", metrics=telemetry
)
assert await worker.run_once() is True
result_points = _metric_points(reader, "ragcore_ingestion_job_result_total")
assert len(result_points) == 1
assert dict(result_points[0].attributes) == {
"operation": "ingest_upload",
"result": "terminal_failed",
}
@@ -0,0 +1,127 @@
"""A worker process runs more than one job at a time, or it does not scale.
``claim_next`` has always used ``FOR UPDATE SKIP LOCKED``, so several claim
loops can safely draw from the same queue -- but nothing ever ran more than
one. A folder source with a few thousand files was therefore ingested strictly
one document after another.
The test below cannot pass on a serial pool: each handler waits until every
other one has started before it returns, so if the loops take turns the whole
thing deadlocks instead of finishing.
"""
from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from uuid import uuid4
import pytest
from ragcore.application.jobs.models import Job, JobClaim, JobContext, JobError
from ragcore.application.jobs.worker import run_worker_pool
CONCURRENCY = 3
def _job(index: int) -> Job:
return Job(
id=uuid4(),
workspace_id=uuid4(),
job_key=f"job-{index}",
operation="ingest_upload",
target_key="blob:abc",
profile_version="upload-v1",
available_at=datetime.now(UTC),
)
class _QueueRepository:
"""Hands every job out exactly once, to whoever asks first."""
def __init__(self, jobs: list[Job]) -> None:
self._pending = list(jobs)
self._expected = len(jobs)
self.completed: list[Job] = []
self.claimed_by: list[str] = []
self.drained = asyncio.Event()
async def claim_next(
self, worker_id: str, *, now: datetime, lease_duration: timedelta
) -> JobClaim | None:
if not self._pending:
return None
job = self._pending.pop(0)
self.claimed_by.append(worker_id)
return JobClaim(job=job, worker_id=worker_id, attempt_number=1, lease_until=now)
async def heartbeat(self, claim: JobClaim, **kwargs: object) -> bool:
return True
async def complete(self, claim: JobClaim, *, now: datetime) -> None:
self.completed.append(claim.job)
if len(self.completed) == self._expected:
self.drained.set()
async def fail(self, claim: JobClaim, error: JobError, **kwargs: object) -> None:
raise AssertionError(f"no job should fail here: {error}")
@pytest.mark.asyncio
async def test_a_worker_pool_runs_jobs_at_the_same_time() -> None:
repository = _QueueRepository([_job(index) for index in range(CONCURRENCY)])
started = asyncio.Barrier(CONCURRENCY)
stop = asyncio.Event()
async def handler(context: JobContext) -> None:
# Returns only once every other handler has also started. A pool that
# runs one job at a time can never get past this.
await started.wait()
async def drain() -> None:
await repository.drained.wait()
stop.set()
await asyncio.wait_for(
asyncio.gather(
run_worker_pool(
repository, # type: ignore[arg-type]
{"ingest_upload": handler},
stop=stop,
concurrency=CONCURRENCY,
worker_id_prefix="pool-test",
poll_interval=0.01,
),
drain(),
),
timeout=5.0,
)
assert len(repository.completed) == CONCURRENCY
# Every loop identifies itself: "which worker holds this lease" is the
# first question an operator asks about a stuck job.
assert len(set(repository.claimed_by)) == CONCURRENCY
@pytest.mark.asyncio
async def test_a_pool_of_one_is_the_previous_behaviour() -> None:
repository = _QueueRepository([_job(0)])
stop = asyncio.Event()
async def handler(context: JobContext) -> None:
stop.set()
await asyncio.wait_for(
run_worker_pool(
repository, # type: ignore[arg-type]
{"ingest_upload": handler},
stop=stop,
concurrency=1,
worker_id_prefix="pool-test",
poll_interval=0.01,
),
timeout=5.0,
)
assert len(repository.completed) == 1
assert repository.claimed_by == ["pool-test-0"]
@@ -0,0 +1,118 @@
from __future__ import annotations
from uuid import uuid4
import httpx
import pytest
from ragcore.application.models.bootstrap import (
EmbeddingBootstrapError,
EmbeddingBootstrapRequest,
ensure_embedding_profile,
)
from ragcore.application.models.registry import (
EmbeddingProfileNotFoundError,
EmbeddingProfileRegistry,
)
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import EmbeddingProfile, EmbeddingProvider, NormalizationMode
from ragcore.infrastructure.models.memory import InMemoryEmbeddingProfileStore
class FakeClient:
def __init__(self, responses: list[httpx.Response]) -> None:
self.responses = responses
self.requests: list[str] = []
async def post(self, url: str, **kwargs: object) -> httpx.Response:
self.requests.append(url)
return self.responses.pop(0)
def _response(status: int, body: object) -> httpx.Response:
return httpx.Response(status, json=body, request=httpx.Request("POST", "http://test"))
def _request() -> EmbeddingBootstrapRequest:
return EmbeddingBootstrapRequest(
provider=EmbeddingProvider.OLLAMA,
model_identifier="nomic-embed-text:1.5",
base_url="http://ollama",
)
def _existing_profile() -> EmbeddingProfile:
return EmbeddingProfile(
id=uuid4(),
profile="hand-registered",
version="v1",
provider=EmbeddingProvider.OLLAMA,
model_identifier="mxbai-embed-large:1",
model_digest="a" * 64,
dimensions=3,
context_limit=4096,
tokenizer="sentencepiece-v1",
data_policy=DataPolicy.LOCAL_ONLY,
normalization=NormalizationMode.NONE,
)
async def test_noop_when_a_profile_is_already_active() -> None:
store = InMemoryEmbeddingProfileStore()
registry = EmbeddingProfileRegistry(store)
existing = _existing_profile()
await registry.register(existing)
await registry.activate(existing.id)
client = FakeClient([])
await ensure_embedding_profile(registry, _request(), http_client=client) # type: ignore[arg-type]
assert client.requests == []
assert (await registry.active()).id == existing.id
async def test_noop_and_no_raise_when_no_request_and_no_active_profile() -> None:
registry = EmbeddingProfileRegistry(InMemoryEmbeddingProfileStore())
client = FakeClient([])
await ensure_embedding_profile(registry, None, http_client=client) # type: ignore[arg-type]
with pytest.raises(EmbeddingProfileNotFoundError):
await registry.active()
async def test_bootstraps_a_real_profile_from_provider_and_model_probes() -> None:
registry = EmbeddingProfileRegistry(InMemoryEmbeddingProfileStore())
client = FakeClient(
[
_response(200, {"digest": "sha256:" + "b" * 64}),
_response(200, {"embeddings": [[0.1, 0.2, 0.3, 0.4]]}),
]
)
await ensure_embedding_profile(registry, _request(), http_client=client) # type: ignore[arg-type]
active = await registry.active()
assert active.model_identifier == "nomic-embed-text:1.5"
assert active.model_digest == "b" * 64
assert active.dimensions == 4
assert client.requests == ["http://ollama/api/show", "http://ollama/api/embed"]
async def test_bootstrap_raises_when_the_model_is_unreachable() -> None:
registry = EmbeddingProfileRegistry(InMemoryEmbeddingProfileStore())
client = FakeClient([_response(500, {"error": "down"})])
with pytest.raises(EmbeddingBootstrapError):
await ensure_embedding_profile(registry, _request(), http_client=client) # type: ignore[arg-type]
with pytest.raises(EmbeddingProfileNotFoundError):
await registry.active()
async def test_bootstrap_raises_on_malformed_digest() -> None:
registry = EmbeddingProfileRegistry(InMemoryEmbeddingProfileStore())
client = FakeClient([_response(200, {"digest": "not-a-digest"})])
with pytest.raises(EmbeddingBootstrapError):
await ensure_embedding_profile(registry, _request(), http_client=client) # type: ignore[arg-type]
@@ -0,0 +1,593 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from dataclasses import replace
from uuid import UUID, uuid4
import pytest
from ragcore.application.models.selection import (
EmbeddingCutoverEvidence,
EmbeddingCutoverRejected,
EmbeddingRollbackEvidence,
EmbeddingSelectionConflict,
EmbeddingSelectionIntegrityError,
EmbeddingSelectionNotInitialized,
EmbeddingSelectionOperationResult,
EmbeddingSelectionService,
EmbeddingSelectionState,
EmbeddingSelectionUnavailable,
EmbeddingSelectionVerificationResult,
)
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import (
EmbeddingProfile,
EmbeddingProvider,
EmbeddingSelection,
NormalizationMode,
)
from ragcore.infrastructure.models.memory import InMemoryEmbeddingProfileStore
def _profile(
*,
profile: str,
version: str,
provider: EmbeddingProvider = EmbeddingProvider.MODELFORGE,
digest: str = "a" * 64,
space_id: UUID | None = None,
) -> EmbeddingProfile:
return EmbeddingProfile(
id=uuid4(),
profile=profile,
version=version,
provider=provider,
model_identifier=(
"rag.embedding@1"
if provider is EmbeddingProvider.MODELFORGE
else "nomic-embed-text:1.5"
),
model_digest=digest,
dimensions=4,
context_limit=4096,
tokenizer="tokenizer-v1",
data_policy=DataPolicy.LOCAL_ONLY,
normalization=NormalizationMode.L2,
embedding_space_id=space_id,
)
class MemorySelectionStore:
"""Small lock-backed fake with the same sentinel, CAS, and retry semantics."""
def __init__(self) -> None:
self.state: EmbeddingSelectionState | None = None
self.initialized = False
self.operations: dict[UUID, tuple[str, EmbeddingSelectionOperationResult]] = {}
self.lock = asyncio.Lock()
async def get(self) -> EmbeddingSelectionState:
if not self.initialized or self.state is None:
raise EmbeddingSelectionNotInitialized("not initialized")
return self.state
async def initialize(self, selection: EmbeddingSelection) -> EmbeddingSelectionState:
async with self.lock:
if not self.initialized:
self.state = EmbeddingSelectionState(0, selection, None)
self.initialized = True
assert self.state is not None
return self.state
async def find_operation(
self,
operation_id: UUID,
*,
request_fingerprint: str,
) -> EmbeddingSelectionOperationResult | None:
entry = self.operations.get(operation_id)
if entry is None:
return None
old_fingerprint, result = entry
if old_fingerprint != request_fingerprint:
raise EmbeddingSelectionConflict("operation id conflict")
return EmbeddingSelectionOperationResult(
result.operation_id,
result.generation,
result.selection,
replayed=True,
evidence=result.evidence,
)
async def compare_and_swap(
self,
*,
expected_generation: int,
target: EmbeddingSelection,
operation_id: UUID,
action: str,
request_fingerprint: str,
operation_evidence: Mapping[str, object] | None = None,
) -> EmbeddingSelectionOperationResult:
async with self.lock:
replay = await self.find_operation(
operation_id,
request_fingerprint=request_fingerprint,
)
if replay is not None:
return replay
if action == "initialize":
if self.initialized:
raise EmbeddingSelectionConflict("already initialized")
if expected_generation != 0:
raise EmbeddingSelectionConflict("stale")
self.state = EmbeddingSelectionState(expected_generation, target, None)
self.initialized = True
result = EmbeddingSelectionOperationResult(
operation_id,
expected_generation,
target,
evidence=operation_evidence or {},
)
self.operations[operation_id] = (request_fingerprint, result)
return result
state = await self.get()
if state.generation != expected_generation:
raise EmbeddingSelectionConflict("stale")
if action == "rollback" and state.previous != target:
raise EmbeddingSelectionConflict("not persisted prior")
self.state = EmbeddingSelectionState(state.generation + 1, target, state.active)
result = EmbeddingSelectionOperationResult(
operation_id,
self.state.generation,
target,
evidence=operation_evidence or {},
)
self.operations[operation_id] = (request_fingerprint, result)
return result
class UnavailableSelectionStore(MemorySelectionStore):
async def get(self) -> EmbeddingSelectionState:
raise EmbeddingSelectionUnavailable("database is unavailable")
class RecordingVerifier:
def __init__(
self, *, verified_point_count: int = 7, include_gateway_request_id: bool = False
) -> None:
self.verified: list[EmbeddingSelection] = []
self.verified_point_count = verified_point_count
self.gateway_request_ids = (uuid4(),) if include_gateway_request_id else ()
async def verify(
self, selection: EmbeddingSelection, profile: EmbeddingProfile
) -> EmbeddingSelectionVerificationResult:
assert selection.matches_profile(profile)
self.verified.append(selection)
return EmbeddingSelectionVerificationResult(
gateway_request_ids=self.gateway_request_ids,
verified_point_count=self.verified_point_count,
)
def _evidence(selection: EmbeddingSelection) -> EmbeddingCutoverEvidence:
return EmbeddingCutoverEvidence(
comparison_id=uuid4(),
comparison_reviewed=True,
comparison_passed=True,
operator_id=uuid4(),
approval_id=uuid4(),
operator_approved_impact_and_reindex=True,
index_operation_id=uuid4(),
authoritative_reindex_completed=True,
collection_identity_pins_verified=True,
consumer_smoke_passed=True,
candidate=selection,
gateway_route_key=selection.experiment_route_key,
gateway_embedding_space_id=selection.embedding_space_id,
gateway_dimensions=selection.dimensions,
)
async def _service(
*profiles: EmbeddingProfile,
) -> tuple[EmbeddingSelectionService, MemorySelectionStore, RecordingVerifier]:
profile_store = InMemoryEmbeddingProfileStore()
for profile in profiles:
await profile_store.add(profile)
await profile_store.activate(profiles[0].id)
selection_store = MemorySelectionStore()
verifier = RecordingVerifier()
return (
EmbeddingSelectionService(
profiles=profile_store, selections=selection_store, verifier=verifier
),
selection_store,
verifier,
)
async def test_bootstrap_pins_initial_route_and_later_config_cannot_remap_it() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
service, _, _ = await _service(current)
initial = await service.bootstrap(initial_experiment_route_key="lab-current-route")
restarted = await service.bootstrap(initial_experiment_route_key="different-lab-route")
assert initial.generation == restarted.generation == 0
assert restarted.active.experiment_route_key == "lab-current-route"
assert restarted.active.collection_name == "rag_dense_embed_v1"
async def test_explicit_modelforge_initialization_registers_and_cas_pins_exact_rc_identity() -> (
None
):
profile = _profile(
profile="modelforge-rc-con01",
version="rc1",
digest="d" * 64,
space_id=uuid4(),
)
profile_store = InMemoryEmbeddingProfileStore()
selection_store = MemorySelectionStore()
verifier = RecordingVerifier(verified_point_count=12, include_gateway_request_id=True)
service = EmbeddingSelectionService(
profiles=profile_store,
selections=selection_store,
verifier=verifier,
)
operation_id, operator_id = uuid4(), uuid4()
result = await service.initialize_modelforge(
expected_generation=0,
operation_id=operation_id,
operator_id=operator_id,
profile=profile,
experiment_route_key="rc-canary-2026-09",
)
assert result.generation == 0
assert result.selection.profile_id == profile.id
assert result.selection.model_digest == profile.model_digest
assert result.selection.dimensions == profile.dimensions
assert result.selection.embedding_space_id == profile.embedding_space_id
assert result.selection.experiment_route_key == "rc-canary-2026-09"
assert result.selection.collection_name == "rag_dense_modelforge-rc-con01_rc1"
verification = result.evidence["verification"]
assert isinstance(verification, Mapping)
assert len(verification["gateway_request_ids"]) == 1
UUID(verification["gateway_request_ids"][0])
assert verification["verified_point_count"] == 12
assert await profile_store.get(profile.id) == profile
assert await profile_store.get_active() is None
assert selection_store.state == EmbeddingSelectionState(0, result.selection, None)
replay = await service.initialize_modelforge(
expected_generation=0,
operation_id=operation_id,
operator_id=operator_id,
profile=profile,
experiment_route_key="rc-canary-2026-09",
)
assert replay.replayed is True
assert replay.selection == result.selection
assert len(verifier.verified) == 1
with pytest.raises(EmbeddingSelectionConflict, match="operation id conflict"):
await service.initialize_modelforge(
expected_generation=0,
operation_id=operation_id,
operator_id=operator_id,
profile=profile,
experiment_route_key="rc-canary-next",
)
async def test_modelforge_initialization_rejects_existing_ollama() -> None:
current = _profile(
profile="ollama-current",
version="v1",
provider=EmbeddingProvider.OLLAMA,
)
candidate = _profile(
profile="modelforge-rc-con01",
version="rc1",
space_id=uuid4(),
)
profile_store = InMemoryEmbeddingProfileStore()
selection_store = MemorySelectionStore()
await selection_store.initialize(EmbeddingSelection.from_profile(current))
service = EmbeddingSelectionService(
profiles=profile_store,
selections=selection_store,
verifier=RecordingVerifier(),
)
with pytest.raises(EmbeddingSelectionConflict, match="already initialized"):
await service.initialize_modelforge(
expected_generation=0,
operation_id=uuid4(),
operator_id=uuid4(),
profile=candidate,
experiment_route_key="rc-canary-2026-09",
)
assert await profile_store.get(candidate.id) is None
assert selection_store.state == EmbeddingSelectionState(
0, EmbeddingSelection.from_profile(current), None
)
async def test_explicit_modelforge_initialization_requires_verified_route_and_nonempty_index() -> (
None
):
profile = _profile(
profile="modelforge-rc-con01",
version="rc1",
space_id=uuid4(),
)
profile_store = InMemoryEmbeddingProfileStore()
selection_store = MemorySelectionStore()
verifier = RecordingVerifier(verified_point_count=0, include_gateway_request_id=True)
service = EmbeddingSelectionService(
profiles=profile_store,
selections=selection_store,
verifier=verifier,
)
with pytest.raises(EmbeddingCutoverRejected, match="collection is empty"):
await service.initialize_modelforge(
expected_generation=0,
operation_id=uuid4(),
operator_id=uuid4(),
profile=profile,
experiment_route_key="rc-canary-2026-09",
)
assert await profile_store.get(profile.id) is None
with pytest.raises(EmbeddingCutoverRejected, match="must use ModelForge"):
await service.initialize_modelforge(
expected_generation=0,
operation_id=uuid4(),
operator_id=uuid4(),
profile=_profile(profile="ollama", version="v1", provider=EmbeddingProvider.OLLAMA),
experiment_route_key="rc-canary-2026-09",
)
assert selection_store.state is None
async def test_changed_space_cutover_and_rollback_restore_full_prior_selection() -> None:
old_space, new_space = uuid4(), uuid4()
current = _profile(profile="embed", version="v1", space_id=old_space)
candidate = _profile(
profile="embed",
version="v2",
digest="b" * 64,
space_id=new_space,
)
service, store, verifier = await _service(current, candidate)
initial = await service.bootstrap(initial_experiment_route_key="lab-current-route")
prior = initial.active
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-candidate-route")
operation_id = uuid4()
evidence = _evidence(target)
promoted = await service.promote(
expected_generation=0,
operation_id=operation_id,
target=target,
evidence=evidence,
)
assert promoted.generation == 1
assert promoted.selection == target
assert promoted.evidence["verification"] == {
"gateway_request_ids": [],
"verified_point_count": 7,
}
assert store.state is not None and store.state.previous == prior
assert (await service.resolve_active()).selection == target
replay = await service.promote(
expected_generation=0,
operation_id=operation_id,
target=target,
evidence=evidence,
)
assert replay.replayed is True and replay.generation == 1
with pytest.raises(EmbeddingSelectionConflict, match="operation id conflict"):
await service.promote(
expected_generation=0,
operation_id=operation_id,
target=target,
evidence=replace(evidence, approval_id=uuid4()),
)
rolled_back = await service.rollback(
expected_generation=1,
operation_id=uuid4(),
evidence=EmbeddingRollbackEvidence(
operator_id=uuid4(), approval_id=uuid4(), reason="validated candidate regression"
),
)
assert rolled_back.generation == 2
assert rolled_back.selection == prior
assert prior.embedding_space_id == old_space
assert prior.experiment_route_key == "lab-current-route"
assert prior.collection_name == "rag_dense_embed_v1"
assert (await service.resolve_active()).selection == prior
assert verifier.verified == [target, prior]
async def test_concurrent_promotions_use_generation_cas() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate_a = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
candidate_b = _profile(profile="embed", version="v3", digest="c" * 64, space_id=uuid4())
service, store, _ = await _service(current, candidate_a, candidate_b)
await service.bootstrap(initial_experiment_route_key="lab-current-route")
target_a = EmbeddingSelection.from_profile(candidate_a, experiment_route_key="lab-route-a")
target_b = EmbeddingSelection.from_profile(candidate_b, experiment_route_key="lab-route-b")
outcomes = await asyncio.gather(
service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target_a,
evidence=_evidence(target_a),
),
service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target_b,
evidence=_evidence(target_b),
),
return_exceptions=True,
)
assert sum(not isinstance(result, BaseException) for result in outcomes) == 1
failures = [result for result in outcomes if isinstance(result, BaseException)]
assert len(failures) == 1 and isinstance(failures[0], EmbeddingSelectionConflict)
assert store.state is not None and store.state.generation == 1
async def test_stale_route_evidence_and_stale_collection_are_rejected() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
service, _, verifier = await _service(current, candidate)
await service.bootstrap(initial_experiment_route_key="lab-current-route")
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-candidate-route")
evidence = _evidence(target)
stale_route = replace(evidence, gateway_route_key="lab-old-route")
with pytest.raises(EmbeddingCutoverRejected, match="gateway route"):
await service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target,
evidence=stale_route,
)
assert verifier.verified == []
with pytest.raises(ValueError, match="collection does not match"):
EmbeddingSelection(
profile_id=candidate.id,
profile=candidate.profile,
version=candidate.version,
provider=candidate.provider,
model_identifier=candidate.model_identifier,
model_digest=candidate.model_digest,
dimensions=candidate.dimensions,
embedding_space_id=candidate.embedding_space_id,
experiment_route_key="lab-candidate-route",
collection_name="rag_dense_embed_v1",
)
async def test_promotion_requires_a_distinct_candidate_modelforge_route() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
service, store, verifier = await _service(current, candidate)
await service.bootstrap(initial_experiment_route_key="lab-current-route")
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-current-route")
with pytest.raises(EmbeddingCutoverRejected, match="distinct pinned current and candidate"):
await service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target,
evidence=_evidence(target),
)
assert store.state is not None and store.state.generation == 0
assert verifier.verified == []
async def test_promotion_refuses_to_drop_an_unresolvable_prior_profile() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
profile_store = InMemoryEmbeddingProfileStore()
await profile_store.add(candidate)
await profile_store.activate(candidate.id)
selection_store = MemorySelectionStore()
prior = EmbeddingSelection.from_profile(current, experiment_route_key="lab-current-route")
await selection_store.initialize(prior)
verifier = RecordingVerifier()
service = EmbeddingSelectionService(
profiles=profile_store,
selections=selection_store,
verifier=verifier,
)
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-next-route")
with pytest.raises(EmbeddingSelectionIntegrityError, match="unregistered"):
await service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target,
evidence=_evidence(target),
)
assert selection_store.state == EmbeddingSelectionState(0, prior, None)
assert verifier.verified == []
async def test_promotion_rejects_an_empty_candidate_collection() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
service, store, verifier = await _service(current, candidate)
verifier.verified_point_count = 0
await service.bootstrap(initial_experiment_route_key="lab-current-route")
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-candidate-route")
with pytest.raises(EmbeddingCutoverRejected, match="collection is empty"):
await service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target,
evidence=_evidence(target),
)
assert store.state is not None and store.state.generation == 0
assert verifier.verified == [target]
async def test_cutover_evidence_requires_real_boolean_attestations() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
candidate = _profile(profile="embed", version="v2", digest="b" * 64, space_id=uuid4())
service, _, verifier = await _service(current, candidate)
await service.bootstrap(initial_experiment_route_key="lab-current-route")
target = EmbeddingSelection.from_profile(candidate, experiment_route_key="lab-candidate-route")
with pytest.raises(EmbeddingCutoverRejected, match="incomplete or failed"):
await service.promote(
expected_generation=0,
operation_id=uuid4(),
target=target,
evidence=replace(_evidence(target), comparison_passed=1), # type: ignore[arg-type]
)
assert verifier.verified == []
async def test_failed_durable_read_does_not_fall_back_to_json_active_profile() -> None:
current = _profile(profile="embed", version="v1", space_id=uuid4())
service, _, _ = await _service(current)
with pytest.raises(EmbeddingSelectionNotInitialized):
await service.resolve_active()
async def test_database_unavailability_does_not_fall_back_to_json_active_profile() -> None:
profile = _profile(profile="embed", version="v1", space_id=uuid4())
profile_store = InMemoryEmbeddingProfileStore()
await profile_store.add(profile)
await profile_store.activate(profile.id)
service = EmbeddingSelectionService(
profiles=profile_store,
selections=UnavailableSelectionStore(),
verifier=RecordingVerifier(),
)
with pytest.raises(EmbeddingSelectionUnavailable, match="database is unavailable"):
await service.resolve_active()
@@ -0,0 +1,184 @@
from __future__ import annotations
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from ragcore.application.profiles import (
ActivationDecision,
AlreadyRolledBackError,
CandidateAliasTarget,
IncompleteEvaluationError,
NotActivatedError,
ProfileActivationError,
require_complete_run,
)
from ragcore.evaluation import EvaluationCase, EvaluationDataset, EvaluationRun, RetrievalRunProfile
from ragcore.evaluation.metrics.models import GateResult, RegressionFinding, RegressionMetric
from ragcore.evaluation.models import AnswerabilityStatus, CaseResult
def _now() -> datetime:
return datetime.now(UTC)
def _run_profile(index_version: str = "v1") -> RetrievalRunProfile:
return RetrievalRunProfile(
embedding_profile_digest="a" * 64,
sparse_profile_version="technical-v1",
index_alias="rag_dense_hybrid_active",
index_collection_version=index_version,
)
def _passing_gate() -> GateResult:
return GateResult(thresholds_version="v1", passed=True, regressions=())
def _target() -> CandidateAliasTarget:
return CandidateAliasTarget(
alias_name="rag_dense_hybrid_active",
collection_name="rag_dense_hybrid_v2",
version="v2",
model_digest="b" * 64,
)
def _decision(**overrides: object) -> ActivationDecision:
values: dict[str, object] = {
"id": uuid4(),
"created_at": _now(),
"dataset_id": "sample",
"dataset_version": "v1",
"baseline_run_id": uuid4(),
"candidate_run_id": uuid4(),
"baseline_profile": _run_profile("v1"),
"candidate_profile": _run_profile("v2"),
"gate": _passing_gate(),
"activated": True,
"candidate_target": _target(),
"index_operation_id": uuid4(),
"previous_collection": "rag_dense_hybrid_v1",
}
values.update(overrides)
return ActivationDecision(**values)
def test_candidate_alias_target_rejects_unsafe_identifiers() -> None:
with pytest.raises(ProfileActivationError):
CandidateAliasTarget(
alias_name="not a safe id!", collection_name="c", version="v1", model_digest="d"
)
def test_activated_decision_requires_candidate_target_and_operation_id() -> None:
with pytest.raises(ProfileActivationError):
_decision(candidate_target=None)
with pytest.raises(ProfileActivationError):
_decision(index_operation_id=None)
def test_rejected_decision_must_not_carry_activation_fields() -> None:
with pytest.raises(ProfileActivationError):
_decision(activated=False, gate=_failing_gate())
def _failing_gate() -> GateResult:
return GateResult(
thresholds_version="v1",
passed=False,
regressions=(
RegressionFinding(
metric=RegressionMetric.RECALL_AT_5,
tag=None,
baseline_value=0.9,
candidate_value=0.5,
delta=-0.4,
),
),
)
def test_failed_gate_can_never_be_recorded_as_activated() -> None:
with pytest.raises(ProfileActivationError):
_decision(gate=_failing_gate(), activated=True)
def test_rollback_requires_an_activated_decision() -> None:
rejected = _decision(
activated=False, gate=_failing_gate(), candidate_target=None, index_operation_id=None
)
with pytest.raises(NotActivatedError):
rejected.with_rollback(rolled_back_at=_now())
def test_rollback_cannot_happen_twice() -> None:
decision = _decision()
rolled_back = decision.with_rollback(rolled_back_at=_now())
assert rolled_back.rolled_back is True
with pytest.raises(AlreadyRolledBackError):
rolled_back.with_rollback(rolled_back_at=_now())
def _dataset() -> EvaluationDataset:
case = EvaluationCase(
id="case-1",
question="What restarts the payment service?",
language="en",
expected_answerability=AnswerabilityStatus.ANSWERABLE,
expected_document_ids=(uuid4(),),
expected_chunk_ids=(uuid4(),),
required_facts=("restart via kubectl rollout restart",),
)
return EvaluationDataset(id="sample", version="v1", cases=(case,))
def _run(dataset: EvaluationDataset, *, cover_all: bool = True) -> EvaluationRun:
case_results = tuple(
CaseResult(
case_id=case.id,
retrieved_chunk_ids=case.expected_chunk_ids,
matched_expected_ids=case.expected_chunk_ids,
missing_expected_ids=(),
unexpected_ids=(),
)
for case in dataset.cases
)
if not cover_all:
case_results = case_results[:0]
return EvaluationRun(
id=uuid4(),
dataset_id=dataset.id,
dataset_version=dataset.version,
run_profile=_run_profile(),
started_at=_now(),
case_results=case_results or (
CaseResult(
case_id="unrelated-case",
retrieved_chunk_ids=(),
matched_expected_ids=(),
missing_expected_ids=(),
unexpected_ids=(),
),
),
)
def test_require_complete_run_accepts_full_coverage() -> None:
dataset = _dataset()
require_complete_run(dataset, _run(dataset))
def test_require_complete_run_rejects_partial_coverage() -> None:
dataset = _dataset()
with pytest.raises(IncompleteEvaluationError):
require_complete_run(dataset, _run(dataset, cover_all=False))
def test_require_complete_run_rejects_wrong_dataset_version() -> None:
dataset = _dataset()
run = _run(dataset)
other_dataset = EvaluationDataset(id="sample", version="v2", cases=dataset.cases)
with pytest.raises(IncompleteEvaluationError):
require_complete_run(other_dataset, run)
@@ -0,0 +1,371 @@
"""Fast unit coverage for ProfileActivationService using in-memory index stores.
Reuses the real ``IndexBuildService`` (never reimplemented), backed by
in-memory fakes for ``DerivedIndexStore``/``IndexOperationStore`` instead of
real Qdrant/Postgres -- this proves ProfileActivationService's own gating,
decision-persistence and rollback-reconstruction logic quickly and
deterministically. Slower, real-infrastructure end-to-end coverage lives in
``tests/integration/profiles``.
"""
from __future__ import annotations
from collections.abc import AsyncIterator, Mapping, Sequence
from datetime import UTC, datetime, timedelta
from uuid import UUID, uuid4
import pytest
from ragcore.application.indexes import (
IndexBuildService,
IndexOperationStatus,
MalformedIndexPoint,
StoredIndexPoint,
)
from ragcore.application.profiles import (
ActivationDecisionNotFoundError,
ActivationRejectedError,
AlreadyRolledBackError,
CandidateAliasTarget,
IncompleteEvaluationError,
ProfileActivationService,
)
from ragcore.evaluation import EvaluationCase, EvaluationDataset, EvaluationRun, RetrievalRunProfile
from ragcore.evaluation.metrics.models import RegressionThresholds
from ragcore.evaluation.models import AnswerabilityStatus, CaseResult
class InMemoryDerivedIndexStore:
def __init__(self) -> None:
self.aliases: dict[str, str] = {}
self.shadows: set[str] = set()
self.build_calls = 0
self.rollback_calls: list[tuple[str, str | None]] = []
async def create_shadow(self, profile: object) -> None:
self.shadows.add(profile.collection_name) # type: ignore[attr-defined]
self.build_calls += 1
async def delete_shadow(self, profile: object) -> None:
self.shadows.discard(profile.collection_name) # type: ignore[attr-defined]
async def upsert_points(self, profile: object, points: Sequence[object]) -> None:
return None
async def delete_points(self, profile: object, point_ids: Sequence[UUID]) -> None:
return None
async def delete_raw_points(self, profile: object, raw_ids: Sequence[str]) -> None:
return None
async def count_points(self, profile: object) -> int:
return 0
async def list_points(
self, profile: object
) -> tuple[tuple[StoredIndexPoint, ...], tuple[MalformedIndexPoint, ...]]:
return (), ()
async def current_alias_target(self, alias_name: str) -> str | None:
return self.aliases.get(alias_name)
async def activate(self, profile: object) -> None:
self.aliases[profile.alias_name] = profile.collection_name # type: ignore[attr-defined]
async def rollback(self, alias_name: str, previous_collection: str | None) -> None:
self.rollback_calls.append((alias_name, previous_collection))
if previous_collection is None:
self.aliases.pop(alias_name, None)
else:
self.aliases[alias_name] = previous_collection
class InMemoryIndexOperationStore:
def __init__(self) -> None:
self._rows: dict[UUID, dict[str, object]] = {}
async def begin(
self,
*,
alias_name: str,
operation: str,
collection_name: str,
owner_token: str,
now: datetime,
lease_duration: timedelta,
) -> UUID | None:
for row in self._rows.values():
in_progress = row["status"] == IndexOperationStatus.IN_PROGRESS
if row["alias_name"] == alias_name and in_progress:
return None
operation_id = uuid4()
self._rows[operation_id] = {
"alias_name": alias_name,
"operation": operation,
"collection_name": collection_name,
"owner_token": owner_token,
"status": IndexOperationStatus.IN_PROGRESS,
"previous_collection": None,
}
return operation_id
async def set_previous_collection(
self,
operation_id: UUID,
owner_token: str,
previous_collection: str | None,
*,
now: datetime,
) -> None:
self._rows[operation_id]["previous_collection"] = previous_collection
async def record_step(
self,
operation_id: UUID,
owner_token: str,
step: str,
*,
now: datetime,
details: Mapping[str, object] | None = None,
) -> None:
return None
async def finish(
self,
operation_id: UUID,
owner_token: str,
status: IndexOperationStatus,
*,
now: datetime,
error_code: str | None = None,
error_details: Mapping[str, object] | None = None,
) -> bool:
self._rows[operation_id]["status"] = status
return True
class FixtureSource:
def __init__(self) -> None:
pass
async def iter_points(self, profile: object) -> AsyncIterator[object]:
return
yield # pragma: no cover - makes this an async generator with zero points
class PassingSmoke:
async def run(self, profile: object) -> None:
return None
class InMemoryActivationDecisionStore:
def __init__(self) -> None:
self._decisions: dict[UUID, object] = {}
async def save(self, decision: object) -> None:
self._decisions[decision.id] = decision # type: ignore[attr-defined]
async def get(self, decision_id: UUID) -> object | None:
return self._decisions.get(decision_id)
async def list_recent(self, limit: int = 20) -> tuple[object, ...]:
return tuple(self._decisions.values())[:limit]
async def mark_rolled_back(self, decision_id: UUID, *, rolled_back_at: datetime) -> None:
decision = self._decisions[decision_id]
self._decisions[decision_id] = decision.with_rollback(rolled_back_at=rolled_back_at) # type: ignore[attr-defined]
def _dataset() -> EvaluationDataset:
case = EvaluationCase(
id="case-1",
question="How do I restart the payment service?",
language="en",
expected_answerability=AnswerabilityStatus.ANSWERABLE,
expected_document_ids=(uuid4(),),
expected_chunk_ids=(uuid4(),),
required_facts=("restart via kubectl rollout restart",),
)
return EvaluationDataset(id="sample", version="v1", cases=(case,))
def _run_profile(version: str) -> RetrievalRunProfile:
return RetrievalRunProfile(
embedding_profile_digest="a" * 64,
sparse_profile_version="technical-v1",
index_alias="rag_dense_hybrid_active",
index_collection_version=version,
)
def _complete_run(dataset: EvaluationDataset, *, version: str, hit: bool = True) -> EvaluationRun:
case = dataset.cases[0]
retrieved = case.expected_chunk_ids if hit else ()
return EvaluationRun(
id=uuid4(),
dataset_id=dataset.id,
dataset_version=dataset.version,
run_profile=_run_profile(version),
started_at=datetime.now(UTC),
case_results=(
CaseResult(
case_id=case.id,
retrieved_chunk_ids=retrieved,
matched_expected_ids=retrieved,
missing_expected_ids=() if hit else case.expected_chunk_ids,
unexpected_ids=(),
),
),
)
def _incomplete_run(dataset: EvaluationDataset) -> EvaluationRun:
return EvaluationRun(
id=uuid4(),
dataset_id=dataset.id,
dataset_version=dataset.version,
run_profile=_run_profile("v2"),
started_at=datetime.now(UTC),
case_results=(
CaseResult(
case_id="a-different-case",
retrieved_chunk_ids=(),
matched_expected_ids=(),
missing_expected_ids=(),
unexpected_ids=(),
),
),
)
def _target() -> CandidateAliasTarget:
return CandidateAliasTarget(
alias_name="rag_dense_hybrid_active",
collection_name="rag_dense_hybrid_v2",
version="v2",
model_digest="b" * 64,
)
_ServiceFixture = tuple[
ProfileActivationService, InMemoryDerivedIndexStore, InMemoryActivationDecisionStore
]
def _service() -> _ServiceFixture:
store = InMemoryDerivedIndexStore()
builder = IndexBuildService(store, InMemoryIndexOperationStore())
decisions = InMemoryActivationDecisionStore()
return ProfileActivationService(index_builder=builder, decisions=decisions), store, decisions
@pytest.mark.asyncio
async def test_incomplete_candidate_run_is_rejected_before_any_qdrant_call() -> None:
service, store, decisions = _service()
dataset = _dataset()
baseline = _complete_run(dataset, version="v1")
with pytest.raises(IncompleteEvaluationError):
await service.activate(
dataset=dataset,
baseline_run=baseline,
candidate_run=_incomplete_run(dataset),
thresholds=RegressionThresholds(version="v1"),
candidate_profile=_target(),
source=FixtureSource(),
smoke=PassingSmoke(),
)
assert store.build_calls == 0
assert await decisions.list_recent() == ()
@pytest.mark.asyncio
async def test_a_regressed_candidate_is_rejected_and_recorded_without_activating() -> None:
service, store, decisions = _service()
dataset = _dataset()
baseline = _complete_run(dataset, version="v1", hit=True)
candidate = _complete_run(dataset, version="v2", hit=False)
with pytest.raises(ActivationRejectedError) as excinfo:
await service.activate(
dataset=dataset,
baseline_run=baseline,
candidate_run=candidate,
thresholds=RegressionThresholds(version="v1"),
candidate_profile=_target(),
source=FixtureSource(),
smoke=PassingSmoke(),
)
assert excinfo.value.gate.passed is False
assert store.build_calls == 0
recorded = (await decisions.list_recent())[0]
assert recorded.activated is False
assert recorded.candidate_target is None
@pytest.mark.asyncio
async def test_a_passing_candidate_is_activated_and_recorded() -> None:
service, store, decisions = _service()
dataset = _dataset()
baseline = _complete_run(dataset, version="v1", hit=True)
candidate = _complete_run(dataset, version="v2", hit=True)
decision = await service.activate(
dataset=dataset,
baseline_run=baseline,
candidate_run=candidate,
thresholds=RegressionThresholds(version="v1"),
candidate_profile=_target(),
source=FixtureSource(),
smoke=PassingSmoke(),
)
assert decision.activated is True
assert store.aliases["rag_dense_hybrid_active"] == "rag_dense_hybrid_v2"
stored = await decisions.get(decision.id)
assert stored is not None
assert stored.index_operation_id == decision.index_operation_id
@pytest.mark.asyncio
async def test_rollback_restores_the_previous_alias_and_marks_the_decision() -> None:
service, store, _decisions = _service()
dataset = _dataset()
baseline = _complete_run(dataset, version="v1", hit=True)
candidate = _complete_run(dataset, version="v2", hit=True)
decision = await service.activate(
dataset=dataset,
baseline_run=baseline,
candidate_run=candidate,
thresholds=RegressionThresholds(version="v1"),
candidate_profile=_target(),
source=FixtureSource(),
smoke=PassingSmoke(),
)
assert decision.previous_collection is None # nothing was active before this test
rolled_back = await service.rollback(decision.id)
assert rolled_back.rolled_back is True
assert store.rollback_calls == [("rag_dense_hybrid_active", None)]
assert "rag_dense_hybrid_active" not in store.aliases
@pytest.mark.asyncio
async def test_rollback_twice_is_rejected() -> None:
service, _, _ = _service()
dataset = _dataset()
decision = await service.activate(
dataset=dataset,
baseline_run=_complete_run(dataset, version="v1", hit=True),
candidate_run=_complete_run(dataset, version="v2", hit=True),
thresholds=RegressionThresholds(version="v1"),
candidate_profile=_target(),
source=FixtureSource(),
smoke=PassingSmoke(),
)
await service.rollback(decision.id)
with pytest.raises(AlreadyRolledBackError):
await service.rollback(decision.id)
@pytest.mark.asyncio
async def test_rollback_of_unknown_decision_is_rejected() -> None:
service, _, _ = _service()
with pytest.raises(ActivationDecisionNotFoundError):
await service.rollback(uuid4())
@@ -0,0 +1,500 @@
"""RestoreGateService against fake ports: manifest verification, reconciliation,
golden queries, cross-space denial, and the never-touch-the-source guarantee."""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
from pathlib import Path
from uuid import UUID, uuid4
import pytest
from ragcore.application.backup.models import BackupStatus
from ragcore.application.restore.models import GoldenQueryProbe, RestoreReport, SecurityDenialProbe
from ragcore.application.restore.service import RestoreGateService, read_verified_manifest
from ragcore.domain.citations import Citation
from ragcore.domain.control import DataPolicy, Grant, KnowledgeSpace, Role, UserGrant
SOURCE_DB_URL = "postgresql+psycopg://ragcore:ragcore@postgres:5432/source"
def _write_backup(
tmp_path: Path,
*,
collection_name: str = "docs_v1",
alias: str = "docs-live",
point_ids: Sequence[UUID] = (),
status: BackupStatus = BackupStatus.COMPLETE,
) -> tuple[Path, dict[Path, dict[UUID, Mapping[str, object]]]]:
backup_dir = tmp_path / "backup"
backup_dir.mkdir()
dump_bytes = b"pg-dump-content"
dump_path = backup_dir / "postgres.dump"
dump_path.write_bytes(dump_bytes)
snapshot_bytes = b"qdrant-snapshot-content"
snapshot_name = f"{collection_name}-1234-2026-01-01.snapshot"
snapshot_path = backup_dir / "qdrant" / snapshot_name
snapshot_path.parent.mkdir(parents=True)
snapshot_path.write_bytes(snapshot_bytes)
def _checksum(path: Path, content: bytes) -> dict[str, object]:
return {
"relative_path": str(path.relative_to(backup_dir)),
"sha256": hashlib.sha256(content).hexdigest(),
"size_bytes": len(content),
}
manifest = {
"backup_id": "backup-1",
"status": status.value,
"created_at": datetime.now(UTC).isoformat(),
"software_version": "0.0.0",
"schema_revision": "abc123",
"active_profiles": {},
"postgres_dump": _checksum(dump_path, dump_bytes),
"qdrant_snapshots": [_checksum(snapshot_path, snapshot_bytes)],
"collection_aliases": {alias: collection_name},
"blob_artifacts": [],
"parsed_artifacts": [],
"counts": {},
}
(backup_dir / "manifest.json").write_text(json.dumps(manifest))
snapshot_contents = {
snapshot_path: {
point_id: _point_payload(point_id) for point_id in point_ids
}
}
return backup_dir, snapshot_contents
def _point_payload(point_id: UUID) -> Mapping[str, object]:
return {
"workspace_id": str(uuid4()),
"space_id": str(uuid4()),
"document_id": str(uuid4()),
"document_version_id": str(uuid4()),
"chunk_id": str(point_id),
"location": {"page": 1},
"text": f"content for {point_id}",
}
class _FakePostgresRestorer:
def __init__(self, *, revision: str = "abc123") -> None:
self._revision = revision
self.created_databases: list[str] = []
self.restored_dumps: list[Path] = []
async def create_empty_database(self, *, database_name: str) -> str:
self.created_databases.append(database_name)
return f"postgresql+psycopg://ragcore:ragcore@postgres:5432/{database_name}"
async def restore_dump(self, *, dump_path: Path, target_database_url: str) -> None:
self.restored_dumps.append(dump_path)
async def upgrade_to_head(self, *, target_database_url: str) -> str:
return self._revision
async def drop_database(self, *, database_name: str) -> None:
pass
class _FakeQdrantRestorer:
def __init__(
self,
*,
snapshot_contents: Mapping[Path, Mapping[UUID, Mapping[str, object]]],
source_collections: Mapping[str, Mapping[UUID, Mapping[str, object]]],
nearest_by_vector: Mapping[tuple[float, ...], UUID] | None = None,
) -> None:
self._snapshot_contents = snapshot_contents
self._collections: dict[str, dict[UUID, Mapping[str, object]]] = {
name: dict(points) for name, points in source_collections.items()
}
self._nearest_by_vector = nearest_by_vector or {}
self.restore_calls: list[tuple[Path, str]] = []
self.alias_calls: list[dict[str, str]] = []
async def restore_collection(self, *, snapshot_path: Path, collection_name: str) -> None:
self.restore_calls.append((snapshot_path, collection_name))
self._collections[collection_name] = dict(self._snapshot_contents[snapshot_path])
async def restore_aliases(self, *, alias_to_collection: Mapping[str, str]) -> None:
self.alias_calls.append(dict(alias_to_collection))
async def scroll_point_ids(self, *, collection_name: str) -> frozenset[UUID]:
return frozenset(self._collections.get(collection_name, {}).keys())
async def fetch_point_payload(
self, *, collection_name: str, point_id: UUID
) -> Mapping[str, object] | None:
return self._collections.get(collection_name, {}).get(point_id)
async def search_nearest(
self, *, collection_name: str, query_vector: Sequence[float], limit: int = 1
) -> tuple[UUID, ...]:
match = self._nearest_by_vector.get(tuple(query_vector))
if match is None or match not in self._collections.get(collection_name, {}):
return ()
return (match,)
async def drop_collection(self, *, collection_name: str) -> None:
self._collections.pop(collection_name, None)
class _FakeFileCopier:
async def copy_tree(self, *, source: Path, destination: Path) -> tuple:
return ()
class _FakeControlPlaneReader:
def __init__(
self,
*,
spaces_by_url: Mapping[str, tuple[KnowledgeSpace, ...]],
grants_by_url: Mapping[str, tuple[Grant, ...]] | None = None,
user_grants_by_url: Mapping[str, tuple[UserGrant, ...]] | None = None,
) -> None:
self._spaces_by_url = spaces_by_url
self._grants_by_url = grants_by_url or {}
self._user_grants_by_url = user_grants_by_url or {}
async def list_spaces(
self, *, database_url: str, workspace_id: UUID
) -> tuple[KnowledgeSpace, ...]:
return self._spaces_by_url.get(database_url, ())
async def list_grants(self, *, database_url: str, workspace_id: UUID) -> tuple[Grant, ...]:
return self._grants_by_url.get(database_url, ())
async def list_user_grants(
self, *, database_url: str, workspace_id: UUID
) -> tuple[UserGrant, ...]:
return self._user_grants_by_url.get(database_url, ())
def _space(workspace_id: UUID, space_id: UUID) -> KnowledgeSpace:
return KnowledgeSpace(
id=space_id,
workspace_id=workspace_id,
name=f"space-{space_id.hex[:8]}",
slug=f"space-{space_id.hex[:8]}",
data_policy=DataPolicy.LOCAL_ONLY,
)
async def test_full_run_passes_when_restore_faithfully_reproduces_the_source(
tmp_path: Path,
) -> None:
point_id = uuid4()
backup_dir, snapshot_contents = _write_backup(tmp_path, point_ids=(point_id,))
query_vector = (0.1, 0.2, 0.3, 0.4)
payload = next(iter(next(iter(snapshot_contents.values())).values()))
workspace_id = uuid4()
principal_id = uuid4()
granted_space_id = uuid4()
denied_space_id = uuid4()
disposable_db_url = "postgresql+psycopg://ragcore:ragcore@postgres:5432/restore-1"
both_spaces = (_space(workspace_id, granted_space_id), _space(workspace_id, denied_space_id))
control_plane = _FakeControlPlaneReader(
spaces_by_url={
SOURCE_DB_URL: both_spaces,
disposable_db_url: both_spaces,
},
user_grants_by_url={
disposable_db_url: (
UserGrant(
id=uuid4(),
workspace_id=workspace_id,
user_id=principal_id,
knowledge_space_id=granted_space_id,
role=Role.VIEWER,
),
)
},
)
qdrant = _FakeQdrantRestorer(
snapshot_contents=snapshot_contents,
source_collections={"docs_v1": {point_id: payload}},
nearest_by_vector={query_vector: point_id},
)
service = RestoreGateService(
postgres=_FakePostgresRestorer(),
qdrant=qdrant,
control_plane=control_plane,
files=_FakeFileCopier(),
)
expected_citation = Citation.create(
workspace_id=UUID(str(payload["workspace_id"])),
space_id=UUID(str(payload["space_id"])),
document_id=UUID(str(payload["document_id"])),
document_version_id=UUID(str(payload["document_version_id"])),
chunk_id=UUID(str(payload["chunk_id"])),
parent_chunk_id=None,
location=dict(payload["location"]), # type: ignore[arg-type]
excerpt=str(payload["text"]),
source_uri="ragcore://source/restore-gate",
acquired_at=datetime.now(UTC),
retrieval_run_id=uuid4(),
index_alias="docs-live",
profile_version="restore-gate",
)
report = await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-1",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(
GoldenQueryProbe(
collection_alias="docs-live",
point_id=point_id,
query_vector=query_vector,
expected_excerpt_checksum=expected_citation.excerpt_checksum,
),
),
security_probe=SecurityDenialProbe(
principal_id=principal_id,
workspace_id=workspace_id,
granted_space_id=granted_space_id,
denied_space_id=denied_space_id,
),
)
assert isinstance(report, RestoreReport)
assert report.passed
assert report.reconciliation[0].matches
assert report.golden_queries[0].passed
assert report.security_denial is not None
assert report.security_denial.passed
assert qdrant.restore_calls == [
(backup_dir / "qdrant" / "docs_v1-1234-2026-01-01.snapshot", "disposable-docs_v1")
]
assert qdrant.alias_calls == [{"docs-live": "disposable-docs_v1"}]
async def test_incomplete_manifest_is_rejected_before_touching_any_infra(tmp_path: Path) -> None:
backup_dir, _ = _write_backup(tmp_path, status=BackupStatus.INCOMPLETE)
postgres = _FakePostgresRestorer()
service = RestoreGateService(
postgres=postgres,
qdrant=_FakeQdrantRestorer(snapshot_contents={}, source_collections={}),
control_plane=_FakeControlPlaneReader(spaces_by_url={}),
files=_FakeFileCopier(),
)
with pytest.raises(Exception, match="not 'complete'"):
await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-2",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(),
security_probe=SecurityDenialProbe(
principal_id=uuid4(),
workspace_id=uuid4(),
granted_space_id=uuid4(),
denied_space_id=uuid4(),
),
)
assert postgres.created_databases == []
async def test_corrupted_artifact_checksum_is_rejected_before_touching_any_infra(
tmp_path: Path,
) -> None:
backup_dir, _ = _write_backup(tmp_path)
(backup_dir / "postgres.dump").write_bytes(b"corrupted-bytes")
postgres = _FakePostgresRestorer()
service = RestoreGateService(
postgres=postgres,
qdrant=_FakeQdrantRestorer(snapshot_contents={}, source_collections={}),
control_plane=_FakeControlPlaneReader(spaces_by_url={}),
files=_FakeFileCopier(),
)
with pytest.raises(Exception, match="checksum mismatch"):
await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-3",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(),
security_probe=SecurityDenialProbe(
principal_id=uuid4(),
workspace_id=uuid4(),
granted_space_id=uuid4(),
denied_space_id=uuid4(),
),
)
assert postgres.created_databases == []
async def test_reconciliation_reports_a_point_missing_from_the_restored_snapshot(
tmp_path: Path,
) -> None:
present_id = uuid4()
missing_id = uuid4()
backup_dir, snapshot_contents = _write_backup(tmp_path, point_ids=(present_id,))
workspace_id = uuid4()
qdrant = _FakeQdrantRestorer(
snapshot_contents=snapshot_contents,
# The live source has one extra point the snapshot never captured.
source_collections={
"docs_v1": {
present_id: _point_payload(present_id),
missing_id: _point_payload(missing_id),
}
},
)
control_plane = _FakeControlPlaneReader(spaces_by_url={})
service = RestoreGateService(
postgres=_FakePostgresRestorer(),
qdrant=qdrant,
control_plane=control_plane,
files=_FakeFileCopier(),
)
report = await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-4",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(),
security_probe=SecurityDenialProbe(
principal_id=uuid4(),
workspace_id=workspace_id,
granted_space_id=uuid4(),
denied_space_id=uuid4(),
),
)
assert not report.passed
assert report.reconciliation[0].missing_point_ids == frozenset({missing_id})
assert report.reconciliation[0].orphan_point_ids == frozenset()
async def test_golden_query_fails_when_the_expected_chunk_is_not_returned(tmp_path: Path) -> None:
point_id = uuid4()
backup_dir, snapshot_contents = _write_backup(tmp_path, point_ids=(point_id,))
qdrant = _FakeQdrantRestorer(
snapshot_contents=snapshot_contents,
source_collections={"docs_v1": {point_id: _point_payload(point_id)}},
nearest_by_vector={}, # no vector configured -> search_nearest returns nothing
)
service = RestoreGateService(
postgres=_FakePostgresRestorer(),
qdrant=qdrant,
control_plane=_FakeControlPlaneReader(spaces_by_url={}),
files=_FakeFileCopier(),
)
report = await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-5",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(
GoldenQueryProbe(
collection_alias="docs-live",
point_id=point_id,
query_vector=(0.1, 0.2, 0.3, 0.4),
expected_excerpt_checksum="0" * 64,
),
),
security_probe=SecurityDenialProbe(
principal_id=uuid4(),
workspace_id=uuid4(),
granted_space_id=uuid4(),
denied_space_id=uuid4(),
),
)
assert not report.passed
assert not report.golden_queries[0].passed
assert report.golden_queries[0].returned_chunk_id is None
async def test_security_denial_fails_when_the_denied_space_is_still_reachable(
tmp_path: Path,
) -> None:
backup_dir, snapshot_contents = _write_backup(tmp_path)
workspace_id = uuid4()
principal_id = uuid4()
granted_space_id = uuid4()
denied_space_id = uuid4()
disposable_db_url = "postgresql+psycopg://ragcore:ragcore@postgres:5432/restore-6"
control_plane = _FakeControlPlaneReader(
spaces_by_url={
disposable_db_url: (
_space(workspace_id, granted_space_id),
_space(workspace_id, denied_space_id),
)
},
user_grants_by_url={
disposable_db_url: (
# Misconfigured fixture: a grant to the space that should be denied.
UserGrant(
id=uuid4(),
workspace_id=workspace_id,
user_id=principal_id,
knowledge_space_id=denied_space_id,
role=Role.VIEWER,
),
)
},
)
service = RestoreGateService(
postgres=_FakePostgresRestorer(),
qdrant=_FakeQdrantRestorer(snapshot_contents=snapshot_contents, source_collections={}),
control_plane=control_plane,
files=_FakeFileCopier(),
)
report = await service.run(
backup_dir=backup_dir,
source_database_url=SOURCE_DB_URL,
disposable_database_name="restore-6",
disposable_collection_prefix="disposable",
disposable_blob_root=tmp_path / "blobs",
disposable_parsed_root=tmp_path / "parsed",
golden_queries=(),
security_probe=SecurityDenialProbe(
principal_id=principal_id,
workspace_id=workspace_id,
granted_space_id=granted_space_id,
denied_space_id=denied_space_id,
),
)
assert not report.passed
assert report.security_denial is not None
assert not report.security_denial.passed
assert denied_space_id in report.security_denial.effective_space_ids
def test_read_verified_manifest_rejects_a_missing_manifest_file(tmp_path: Path) -> None:
with pytest.raises(Exception, match=r"no manifest\.json"):
read_verified_manifest(tmp_path)
@@ -0,0 +1,526 @@
from __future__ import annotations
import json
import tempfile
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import httpx
import pytest
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.resources import Resource
from qdrant_client.http import models
from ragcore.api.v1.context.service import ContextExecutionRequest, ContextQuery
from ragcore.api.v1.search.service import SearchExecutionRequest, SearchFilterData, SearchQuery
from ragcore.application.models.selection import ResolvedEmbeddingSelection
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
from ragcore.config import Settings
from ragcore.domain.authorization import Principal, PrincipalType
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import EmbeddingProfile, EmbeddingProvider, EmbeddingSelection
from ragcore.infrastructure.models.embedding_factory import create_embedding_adapter_factory
from ragcore.infrastructure.models.persistence import JsonEmbeddingProfileStore
from ragcore.infrastructure.retrieval.production_executor import (
PostgresDocumentTitleLookup,
ProductionContextExecutor,
ProductionSearchExecutor,
RetrievalPipeline,
)
from ragcore.platform.telemetry import TelemetryMetrics
WORKSPACE_ID = uuid4()
SPACE_ID = uuid4()
DOCUMENT_ID = uuid4()
DOCUMENT_VERSION_ID = uuid4()
CHUNK_ID = uuid4()
SOURCE_ID = uuid4()
EMBEDDING_PROFILE = EmbeddingProfile(
id=uuid4(),
profile="hybrid",
version="v1",
provider=EmbeddingProvider.OLLAMA,
model_identifier="nomic-embed-text:v1",
model_digest="0" * 64,
dimensions=2,
context_limit=2048,
tokenizer="cl100k",
data_policy=DataPolicy.LOCAL_ONLY,
)
_PAYLOAD = {
"workspace_id": str(WORKSPACE_ID),
"space_id": str(SPACE_ID),
"document_id": str(DOCUMENT_ID),
"document_version_id": str(DOCUMENT_VERSION_ID),
"chunk_id": str(CHUNK_ID),
"source_id": str(SOURCE_ID),
"parent_chunk_id": None,
"status": "published",
"content_type": "text/plain",
"language": "en",
"trust_level": "source",
"embedding_model_digest": EMBEDDING_PROFILE.model_digest,
"chunk_profile_version": EMBEDDING_PROFILE.version,
"text": "The runbook says to restart the payment service after a failover.",
"location": {"page": 1},
}
class FakeQdrantClient:
def __init__(
self,
*,
alias_target: str = "rag_dense_hybrid_v1",
payload: dict[str, object] = _PAYLOAD,
) -> None:
self.alias_target = alias_target
self.payload = payload
self.query_collections: list[str] = []
async def get_aliases(self, **kwargs: object) -> SimpleNamespace:
return SimpleNamespace(
aliases=[
SimpleNamespace(
alias_name="rag_dense_hybrid_active", collection_name=self.alias_target
)
]
)
async def get_collection(self, collection_name: str, **kwargs: object) -> SimpleNamespace:
return SimpleNamespace(
config=SimpleNamespace(
params=SimpleNamespace(
vectors={"dense": models.VectorParams(size=2, distance=models.Distance.COSINE)},
sparse_vectors={"sparse": models.SparseVectorParams()},
)
)
)
async def query_points(self, **kwargs: object) -> models.QueryResponse:
self.query_collections.append(str(kwargs.get("collection_name")))
return models.QueryResponse(
points=[
models.ScoredPoint(id=str(CHUNK_ID), version=0, score=0.9, payload=self.payload)
]
)
async def retrieve(self, **kwargs: object) -> list[SimpleNamespace]:
ids = kwargs.get("ids", [])
return [SimpleNamespace(id=raw_id, payload=self.payload) for raw_id in ids]
class PayloadlessQdrantClient(FakeQdrantClient):
"""Ranks a point and then does not return its payload.
Qdrant answered the search with this point, so it is in the index -- but
the follow-up ``retrieve`` gives nothing back for it. The pipeline cannot
build a citation without the text, so the hit is dropped; the point of
this double is that the caller must be *told* that happened.
"""
async def retrieve(self, **kwargs: object) -> list[SimpleNamespace]:
return []
class FakeTitleLookup:
def __init__(self, titles: dict | None = None) -> None: # type: ignore[type-arg]
self._titles = {DOCUMENT_ID: "Restart Runbook"} if titles is None else titles
async def titles(self, document_ids: set) -> dict: # type: ignore[type-arg]
return {doc_id: self._titles[doc_id] for doc_id in document_ids if doc_id in self._titles}
def _embed_transport(vector: tuple = (0.1, 0.2)) -> httpx.MockTransport: # type: ignore[type-arg]
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"embeddings": [list(vector)]})
return httpx.MockTransport(handler)
def _failing_transport(status_code: int = 503) -> httpx.MockTransport:
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(status_code, json={"error": "unavailable"})
return httpx.MockTransport(handler)
def _empty_store() -> JsonEmbeddingProfileStore:
path = Path(tempfile.mkdtemp()) / "profiles.json"
return JsonEmbeddingProfileStore(path)
async def _activated_store() -> JsonEmbeddingProfileStore:
store = _empty_store()
await store.add(EMBEDDING_PROFILE)
await store.activate(EMBEDDING_PROFILE.id)
return store
async def _activated_store_for(profile: EmbeddingProfile) -> JsonEmbeddingProfileStore:
store = _empty_store()
await store.add(profile)
await store.activate(profile.id)
return store
class FixedSelectionResolver:
def __init__(self, profile: EmbeddingProfile, *, route: str) -> None:
self.selected = ResolvedEmbeddingSelection(
generation=8,
profile=profile,
selection=EmbeddingSelection.from_profile(profile, experiment_route_key=route),
)
async def resolve_active(self) -> ResolvedEmbeddingSelection:
return self.selected
def _pipeline(
*,
embedding_transport: httpx.MockTransport,
store,
metrics: TelemetryMetrics | None = None,
) -> RetrievalPipeline:
client = httpx.AsyncClient(transport=embedding_transport, follow_redirects=False)
return RetrievalPipeline(
qdrant_client=FakeQdrantClient(), # type: ignore[arg-type]
embedding_profile_store=store,
embedding_adapter_factory=create_embedding_adapter_factory(
settings=Settings(env="test", ollama_url="http://localhost:11434"),
http_client=client,
),
metrics=metrics,
)
def _scope() -> AuthorizedRetrievalScope:
return AuthorizedRetrievalScope(WORKSPACE_ID, frozenset({SPACE_ID}))
def _search_request(query: str = "how do I restart the payment service") -> SearchExecutionRequest:
return SearchExecutionRequest(
principal=Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset()),
query=SearchQuery(query=query, max_results=5, filters=SearchFilterData()),
scope=_scope(),
retrieval_run_id=uuid4(),
)
def _context_request(
query: str = "how do I restart the payment service",
) -> ContextExecutionRequest:
return ContextExecutionRequest(
principal=Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset()),
query=ContextQuery(
query=SearchQuery(query=query, max_results=5, filters=SearchFilterData()),
max_context_tokens=4000,
),
scope=_scope(),
retrieval_run_id=uuid4(),
)
@pytest.mark.asyncio
async def test_pipeline_degrades_when_no_active_embedding_profile() -> None:
pipeline = _pipeline(embedding_transport=_embed_transport(), store=_empty_store())
result = await pipeline.run(
query_text="restart payment",
content_types=(),
languages=(),
scope=_scope(),
max_results=5,
profile="hybrid-v1",
retrieval_run_id=uuid4(),
)
assert result.hits == ()
assert "no_active_embedding_profile" in result.degradation_codes
@pytest.mark.asyncio
async def test_pipeline_records_retrieval_duration_and_zero_result() -> None:
reader = InMemoryMetricReader()
provider = MeterProvider(
resource=Resource.create({"service.name": "test"}), metric_readers=[reader]
)
metrics = TelemetryMetrics(meter_provider=provider)
pipeline = _pipeline(
embedding_transport=_embed_transport(), store=_empty_store(), metrics=metrics
)
await pipeline.run(
query_text="restart payment",
content_types=(),
languages=(),
scope=_scope(),
max_results=5,
profile="hybrid-v1",
retrieval_run_id=uuid4(),
)
data = reader.get_metrics_data()
assert data is not None
names = {
metric.name
for resource_metric in data.resource_metrics
for scope_metric in resource_metric.scope_metrics
for metric in scope_metric.metrics
}
assert "ragcore_retrieval_duration_seconds" in names
assert "ragcore_retrieval_zero_result_total" in names
@pytest.mark.asyncio
async def test_pipeline_degrades_when_embedding_provider_unavailable() -> None:
pipeline = _pipeline(embedding_transport=_failing_transport(), store=await _activated_store())
result = await pipeline.run(
query_text="restart payment",
content_types=(),
languages=(),
scope=_scope(),
max_results=5,
profile="hybrid-v1",
retrieval_run_id=uuid4(),
)
assert result.hits == ()
assert "embedding_provider_unavailable" in result.degradation_codes
@pytest.mark.asyncio
async def test_search_executor_happy_path_returns_titled_hit_with_fused_candidate() -> None:
"""M8-01: no reranking -- the fused RRF ranking is the ranking, not a
degraded fallback, so this must not report degraded and rerank_score is
always None (kept as a field for response-shape compatibility)."""
pipeline = _pipeline(embedding_transport=_embed_transport(), store=await _activated_store())
executor = ProductionSearchExecutor(pipeline, FakeTitleLookup()) # type: ignore[arg-type]
result = await executor.execute(_search_request())
assert result.degraded is False
assert len(result.hits) == 1
hit = result.hits[0]
assert hit.title == "Restart Runbook"
assert "restart the payment service" in hit.text
assert hit.citation.document_id == DOCUMENT_ID
assert hit.citation.workspace_id == WORKSPACE_ID
assert hit.fused.candidate.provenance.chunk_id == CHUNK_ID
assert hit.rerank_score is None
@pytest.mark.asyncio
async def test_search_executor_falls_back_to_untitled_when_title_missing() -> None:
pipeline = _pipeline(embedding_transport=_embed_transport(), store=await _activated_store())
executor = ProductionSearchExecutor(pipeline, FakeTitleLookup(titles={})) # type: ignore[arg-type]
result = await executor.execute(_search_request())
assert result.hits[0].title == "Untitled document"
@pytest.mark.asyncio
async def test_context_executor_happy_path_returns_context_candidate() -> None:
pipeline = _pipeline(embedding_transport=_embed_transport(), store=await _activated_store())
executor = ProductionContextExecutor(pipeline, FakeQdrantClient()) # type: ignore[arg-type]
result = await executor.execute(_context_request())
assert result.degraded is False
assert len(result.candidates) == 1
assert "restart the payment service" in result.candidates[0].text
assert result.candidates[0].citation.document_id == DOCUMENT_ID
@pytest.mark.asyncio
async def test_context_executor_returns_empty_tuple_when_degraded() -> None:
pipeline = _pipeline(embedding_transport=_embed_transport(), store=_empty_store())
executor = ProductionContextExecutor(pipeline, FakeQdrantClient()) # type: ignore[arg-type]
result = await executor.execute(_context_request())
assert result.candidates == ()
assert result.degraded is True
assert "no_active_embedding_profile" in result.degradation_codes
@pytest.mark.asyncio
async def test_rest_search_uses_durable_route_and_collection_selection(
tmp_path: Path,
) -> None:
profile = EmbeddingProfile(
id=uuid4(),
profile="hybrid",
version="v1",
provider=EmbeddingProvider.MODELFORGE,
model_identifier="rag.embedding@1",
model_digest="c" * 64,
dimensions=2,
context_limit=8192,
tokenizer="capability-managed",
data_policy=DataPolicy.LOCAL_ONLY,
embedding_space_id=uuid4(),
)
credential_file = tmp_path / "credential"
credential_file.write_text("mf_" + "x" * 48, encoding="utf-8")
credential_file.chmod(0o600)
seen: list[httpx.Request] = []
def respond(request: httpx.Request) -> httpx.Response:
seen.append(request)
return httpx.Response(
200,
json={
"capability": "rag.embedding@1",
"dimension": profile.dimensions,
"normalized": True,
"embedding_space_id": str(profile.embedding_space_id),
"data": [[1.0, 0.0]],
"request_id": str(uuid4()),
},
)
settings = Settings(
env="test",
ollama_url="http://localhost:11434",
modelforge_base_url="http://modelforge:8000",
modelforge_credential_file=credential_file,
modelforge_embedding_experiment_route="stale-global-route",
)
payload = {
**_PAYLOAD,
"embedding_model_digest": profile.model_digest,
"embedding_space_id": str(profile.embedding_space_id),
}
qdrant = FakeQdrantClient(alias_target="rag_dense_hybrid_stale", payload=payload)
pipeline = RetrievalPipeline(
qdrant_client=qdrant, # type: ignore[arg-type]
embedding_profile_store=await _activated_store_for(profile),
embedding_adapter_factory=create_embedding_adapter_factory(
settings=settings,
http_client=httpx.AsyncClient(
transport=httpx.MockTransport(respond), follow_redirects=False
),
),
embedding_selection_resolver=FixedSelectionResolver(profile, route="rc-canary-2026-09"),
)
executor = ProductionSearchExecutor(pipeline, FakeTitleLookup()) # type: ignore[arg-type]
result = await executor.execute(_search_request())
assert result.degraded is False
assert len(result.hits) == 1
assert qdrant.query_collections
assert set(qdrant.query_collections) == {"rag_dense_hybrid_v1"}
assert len(seen) == 1
assert seen[0].url.path == "/api/v1/capability-experiments/rc-canary-2026-09/invoke"
assert json.loads(seen[0].content) == {
"input": ["how do I restart the payment service"],
"input_type": "query",
}
@pytest.mark.asyncio
async def test_document_title_lookup_returns_empty_dict_for_empty_input() -> None:
lookup = PostgresDocumentTitleLookup(session_factory=None) # type: ignore[arg-type]
assert await lookup.titles(set()) == {}
class RecordingQdrantClient(FakeQdrantClient):
"""Also records every query_points call so filter compilation is inspectable."""
def __init__(self) -> None:
super().__init__()
self.query_calls: list[dict] = [] # type: ignore[type-arg]
async def query_points(self, **kwargs: object) -> models.QueryResponse:
self.query_calls.append(dict(kwargs))
return await super().query_points(**kwargs)
@pytest.mark.asyncio
async def test_search_executor_compiles_every_accepted_filter_into_the_qdrant_query() -> None:
"""Regression: source_ids/document_ids/trust_levels used to be accepted by
the API and silently dropped before ever reaching Qdrant."""
client = RecordingQdrantClient()
embedding_client = httpx.AsyncClient(transport=_embed_transport(), follow_redirects=False)
pipeline = RetrievalPipeline(
qdrant_client=client, # type: ignore[arg-type]
embedding_profile_store=await _activated_store(),
embedding_adapter_factory=create_embedding_adapter_factory(
settings=Settings(env="test", ollama_url="http://localhost:11434"),
http_client=embedding_client,
),
)
executor = ProductionSearchExecutor(pipeline, FakeTitleLookup()) # type: ignore[arg-type]
request = SearchExecutionRequest(
principal=Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset()),
query=SearchQuery(
query="restart",
max_results=5,
filters=SearchFilterData(
source_ids=(SOURCE_ID,),
document_ids=(DOCUMENT_ID,),
trust_levels=("source",),
),
),
scope=_scope(),
retrieval_run_id=uuid4(),
)
await executor.execute(request)
assert client.query_calls
for call in client.query_calls:
compiled = str(call["query_filter"])
assert str(SOURCE_ID) in compiled
assert str(DOCUMENT_ID) in compiled
assert "trust_level" in compiled
@pytest.mark.asyncio
async def test_search_executor_reports_per_stage_timings() -> None:
pipeline = _pipeline(embedding_transport=_embed_transport(), store=await _activated_store())
executor = ProductionSearchExecutor(pipeline, FakeTitleLookup()) # type: ignore[arg-type]
result = await executor.execute(_search_request())
timings = dict(result.timings_ms or {})
for stage in (
"query_embedding",
"sparse_encoding",
"dense_search",
"sparse_search",
"fusion",
"payload_fetch",
):
assert stage in timings
assert timings[stage] >= 0.0
@pytest.mark.asyncio
async def test_pipeline_reports_when_a_ranked_hit_has_no_payload() -> None:
"""A dropped hit is a degradation, not a quietly shorter answer.
Returning four results where the ranking found five, with nothing in the
response to say so, is indistinguishable from "there were only four" --
and the caller acts on it.
"""
embedding_client = httpx.AsyncClient(transport=_embed_transport(), follow_redirects=False)
pipeline = RetrievalPipeline(
qdrant_client=PayloadlessQdrantClient(), # type: ignore[arg-type]
embedding_profile_store=await _activated_store(),
embedding_adapter_factory=create_embedding_adapter_factory(
settings=Settings(env="test", ollama_url="http://localhost:11434"),
http_client=embedding_client,
),
)
result = await pipeline.run(
query_text="restart payment",
content_types=(),
languages=(),
scope=_scope(),
max_results=5,
profile="hybrid-v1",
retrieval_run_id=uuid4(),
)
assert result.hits == ()
assert "payload_unavailable" in result.degradation_codes
@@ -0,0 +1,178 @@
"""Sources that carry an interval get synced without anyone asking.
Until now a sync ran only when an operator pressed the button. For a platform
whose promise is that an index keeps step with a folder or a repository, that
made staleness the default and freshness a chore someone had to remember.
The scheduler does not sync anything. It enqueues the same ``sync_source`` job
the Control Plane button enqueues, so every ownership, convergence and
authorization rule downstream is the one already in place.
"""
from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from uuid import UUID, uuid4
import pytest
from ragcore.application.sources.models import SourceRecord, SourceStatus
from ragcore.application.sources.scheduler import SourceSyncScheduler
from ragcore.domain.sources import ConnectorKind, SourceSyncState
NOW = datetime(2026, 8, 23, 12, 0, tzinfo=UTC)
WORKSPACE = uuid4()
SPACE = uuid4()
def _source(
*,
interval: int | None = 3600,
last_sync_at: datetime | None = None,
status: SourceStatus = SourceStatus.ACTIVE,
sync_state: SourceSyncState = SourceSyncState.COMPLETED,
kind: ConnectorKind = ConnectorKind.FOLDER,
) -> SourceRecord:
return SourceRecord(
id=uuid4(),
workspace_id=WORKSPACE,
space_id=SPACE,
name="Runbooks",
kind=kind,
status=status,
sync_state=sync_state,
last_sync_at=last_sync_at,
sync_interval_seconds=interval,
)
class _Registry:
def __init__(self, sources: list[SourceRecord]) -> None:
self._sources = sources
self.asked_at: list[datetime] = []
async def list_due_for_sync(self, *, now: datetime, limit: int) -> tuple[SourceRecord, ...]:
self.asked_at.append(now)
return tuple(self._sources[:limit])
class _Jobs:
def __init__(self) -> None:
self.enqueued: list[tuple[UUID, str]] = []
self.any_enqueued = asyncio.Event()
async def enqueue(self, **kwargs: object) -> object:
self.enqueued.append((kwargs["workspace_id"], str(kwargs["idempotency_key"]))) # type: ignore[arg-type]
self.any_enqueued.set()
return object()
@pytest.mark.asyncio
async def test_a_due_source_is_enqueued_as_an_ordinary_sync_job() -> None:
due = _source(interval=3600, last_sync_at=NOW - timedelta(hours=2))
jobs = _Jobs()
scheduler = SourceSyncScheduler(_Registry([due]), jobs) # type: ignore[arg-type]
enqueued = await scheduler.tick(now=NOW)
assert enqueued == 1
assert [workspace for workspace, _ in jobs.enqueued] == [WORKSPACE]
@pytest.mark.asyncio
async def test_two_schedulers_ticking_together_produce_one_job() -> None:
"""Several worker replicas each run a scheduler. They must not each start
the same sync -- and they do not need a lock to avoid it: the key is
derived from when the sync came due, so both compute the same one and the
queue's own uniqueness collapses them."""
due = _source(interval=3600, last_sync_at=NOW - timedelta(hours=2))
first, second = _Jobs(), _Jobs()
await SourceSyncScheduler(_Registry([due]), first).tick(now=NOW) # type: ignore[arg-type]
await SourceSyncScheduler(_Registry([due]), second).tick( # type: ignore[arg-type]
now=NOW + timedelta(seconds=17)
)
assert first.enqueued and second.enqueued
assert first.enqueued[0][1] == second.enqueued[0][1]
@pytest.mark.asyncio
async def test_a_source_that_has_never_synced_is_due_immediately() -> None:
jobs = _Jobs()
scheduler = SourceSyncScheduler(_Registry([_source(last_sync_at=None)]), jobs) # type: ignore[arg-type]
assert await scheduler.tick(now=NOW) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize(
"source",
[
pytest.param(_source(interval=None), id="no interval configured"),
pytest.param(
_source(last_sync_at=NOW - timedelta(minutes=5)), id="synced within the interval"
),
pytest.param(_source(status=SourceStatus.PAUSED), id="paused"),
pytest.param(_source(status=SourceStatus.DELETED), id="deleted"),
pytest.param(
_source(sync_state=SourceSyncState.SCANNING), id="a run is already in flight"
),
pytest.param(
_source(sync_state=SourceSyncState.AWAITING_INGEST),
id="waiting on its own child ingests",
),
pytest.param(_source(kind=ConnectorKind.UPLOAD), id="fed by its callers, not synced"),
],
)
async def test_sources_that_must_not_be_scheduled(source: SourceRecord) -> None:
"""The registry query is the first filter, but it is not the only one: the
scheduler refuses these itself, so a widened query cannot quietly start
syncing something that should be left alone."""
jobs = _Jobs()
scheduler = SourceSyncScheduler(_Registry([source]), jobs) # type: ignore[arg-type]
assert await scheduler.tick(now=NOW) == 0
assert jobs.enqueued == []
class _FailingRegistry(_Registry):
def __init__(self, sources: list[SourceRecord]) -> None:
super().__init__(sources)
self.calls = 0
async def list_due_for_sync(self, *, now: datetime, limit: int) -> tuple[SourceRecord, ...]:
self.calls += 1
if self.calls == 1:
raise RuntimeError("the database blinked")
return await super().list_due_for_sync(now=now, limit=limit)
@pytest.mark.asyncio
async def test_a_failing_tick_does_not_end_the_loop() -> None:
"""The scheduler shares a process with the claim loops. A transient
database error while looking for due sources must cost one tick, not the
worker -- the next tick finds the same sources still due."""
registry = _FailingRegistry([_source(last_sync_at=None)])
jobs = _Jobs()
stop = asyncio.Event()
scheduler = SourceSyncScheduler(registry, jobs) # type: ignore[arg-type]
async def until_enqueued() -> None:
await jobs.any_enqueued.wait()
stop.set()
await asyncio.wait_for(
asyncio.gather(
scheduler.run(stop, interval_seconds=0.01),
until_enqueued(),
),
timeout=5.0,
)
assert registry.calls >= 2
assert len(jobs.enqueued) >= 1
@@ -0,0 +1,666 @@
"""Source synchronisation semantics: discover -> compare -> ingest new/changed
-> preserve unchanged -> retire disappeared -> persist the outcome."""
from __future__ import annotations
import hashlib
from collections.abc import AsyncIterable, AsyncIterator
from dataclasses import replace
from datetime import UTC, datetime
from uuid import UUID, uuid4
import pytest
from ragcore.application.documents import DocumentRecord
from ragcore.application.jobs import Job, JobService
from ragcore.application.sources import (
SourceItemRecord,
SourceItemState,
SourceItemSummary,
SourceRecord,
SourceStatus,
SourceSyncOutcome,
SourceSyncRunRecord,
SourceSyncService,
)
from ragcore.application.storage import BlobReference
from ragcore.domain.sources import (
Acquisition,
ConnectorKind,
ScanResult,
SourceCursor,
SourceFingerprint,
SourceItem,
SourceSyncState,
)
WORKSPACE_ID = uuid4()
SPACE_ID = uuid4()
class FakeConnector:
"""Serves a mutable set of path -> content; pages when asked to."""
def __init__(self, source_id: UUID, files: dict[str, bytes], *, page_size: int = 100) -> None:
self.source_id = source_id
self.files = files
self.page_size = page_size
self.acquired: list[str] = []
self.fail_scan = False
def _items(self) -> list[SourceItem]:
return [
SourceItem(
source_id=self.source_id,
external_id=path,
locator=path,
name=path.rsplit("/", 1)[-1],
media_type="text/plain",
metadata={"sha256": hashlib.sha256(content).hexdigest(), "commit": "abc1234def"},
)
for path, content in sorted(self.files.items())
]
async def scan(self, cursor: SourceCursor | None = None) -> ScanResult:
if self.fail_scan:
raise RuntimeError("mount gone")
items = self._items()
start = 0
if cursor is not None and cursor.value:
start = next(i for i, item in enumerate(items) if item.external_id == cursor.value) + 1
page = items[start : start + self.page_size]
has_more = start + self.page_size < len(items)
return ScanResult(
tuple(page),
SourceCursor(page[-1].external_id) if has_more and page else None,
complete=not has_more and cursor is None,
)
async def acquire(self, item: SourceItem) -> Acquisition:
self.acquired.append(item.locator)
content = self.files[item.locator]
async def stream() -> AsyncIterator[bytes]:
yield content
return Acquisition(
stream=stream(),
source_item=item,
fingerprint=SourceFingerprint(hashlib.sha256(content).hexdigest()),
media_type="text/plain",
original_name=item.name,
)
def fingerprint(self, item: SourceItem) -> SourceFingerprint:
return SourceFingerprint(str(item.metadata["sha256"]))
_CONVERGEABLE = (SourceSyncState.AWAITING_INGEST, SourceSyncState.COMPLETED_WITH_FAILURES)
class FakeRegistry:
"""Mirrors PostgresSourceRegistry's convergence semantics, which is the
point of these tests: the counters and the settled state must come out of
remembered items, not out of what discovery happened to see."""
def __init__(self) -> None:
self.items: dict[tuple[UUID, str], SourceItemRecord] = {}
self.runs: dict[UUID, SourceSyncRunRecord] = {}
self.states: list[tuple[SourceSyncState, bool]] = []
self.sync_state = SourceSyncState.IDLE
async def record_sync_run(self, run: SourceSyncRunRecord) -> SourceSyncRunRecord:
self.runs[run.id] = run
return run
async def set_sync_state(
self, source_id, state, *, now, error=None, revision=None, successful=False
):
self.states.append((state, successful))
self.sync_state = state
return None
async def list_items(self, source_id: UUID) -> tuple[SourceItemRecord, ...]:
return tuple(record for (sid, _), record in self.items.items() if sid == source_id)
async def upsert_items(self, items: tuple[SourceItemRecord, ...]) -> None:
for item in items:
self.items[(item.source_id, item.external_id)] = item
async def mark_items_removed(self, source_id, external_ids, *, now) -> None:
for external_id in external_ids:
current = self.items[(source_id, external_id)]
self.items[(source_id, external_id)] = replace(
current,
removed=True,
last_seen_at=now,
# Retirement *is* the applied state: the desired outcome (no
# searchable content) has been reached.
applied_fingerprint=current.fingerprint,
applied_at=now,
pending_job_id=None,
last_failure_code=None,
last_failure_retryable=None,
)
async def mark_item_ingested(
self, source_id, external_id, *, fingerprint, document_id, now
) -> bool:
current = self.items.get((source_id, external_id))
if current is None or current.fingerprint != fingerprint:
return False
self.items[(source_id, external_id)] = replace(
current,
applied_fingerprint=fingerprint,
applied_at=now,
pending_job_id=None,
attempts=0,
last_failure_code=None,
last_failure_retryable=None,
last_failure_job_id=None,
document_id=document_id if document_id is not None else current.document_id,
)
return True
async def mark_item_failed(
self, source_id, external_id, *, fingerprint, failure_code, retryable, now, job_id=None
) -> bool:
current = self.items.get((source_id, external_id))
if current is None or current.fingerprint != fingerprint:
return False
self.items[(source_id, external_id)] = replace(
current,
pending_job_id=None,
last_seen_at=now,
last_failure_code=failure_code[:64],
last_failure_retryable=retryable,
last_failure_job_id=job_id,
)
return True
async def summarize_items(self, source_id: UUID) -> SourceItemSummary:
records = [record for (sid, _), record in self.items.items() if sid == source_id]
live = [record for record in records if not record.removed]
return SourceItemSummary(
total=len(records),
removed=sum(1 for record in records if record.removed),
applied=sum(1 for record in live if record.applied),
failed=sum(1 for record in live if record.state is SourceItemState.FAILED),
pending=sum(1 for record in live if record.state is SourceItemState.PENDING),
)
async def converge_sync_state(self, source_id, state, *, now, successful=False):
if self.sync_state not in _CONVERGEABLE:
return None
self.states.append((state, successful))
self.sync_state = state
return object()
class FakeDocuments:
def __init__(self) -> None:
self.documents: dict[tuple[str, str], DocumentRecord] = {}
self.retired: list[UUID] = []
def seed(self, source_id: UUID, external_id: str) -> DocumentRecord:
document = DocumentRecord(
id=uuid4(),
workspace_id=WORKSPACE_ID,
space_id=SPACE_ID,
source_key=str(source_id),
external_id=external_id,
title=external_id,
source_id=source_id,
)
self.documents[(str(source_id), external_id)] = document
return document
async def get_document_by_source(self, workspace_id, source_key, external_id):
return self.documents.get((source_key, external_id))
async def mark_source_deleted(self, document_id: UUID) -> None:
self.retired.append(document_id)
class FakeBlobs:
def __init__(self) -> None:
self.stored: list[tuple[str, str]] = []
async def put(
self,
chunks: AsyncIterable[bytes],
*,
media_type=None,
original_name=None,
expected_sha256=None,
):
body = b"".join([chunk async for chunk in chunks])
digest = hashlib.sha256(body).hexdigest()
assert expected_sha256 == digest
self.stored.append((original_name or "", digest))
return BlobReference(
digest=digest,
size=len(body),
media_type=media_type,
original_name=original_name,
storage_key=f"blobs/{digest}",
)
class RecordingJobs:
def __init__(self) -> None:
self.jobs: list[Job] = []
async def enqueue(self, job: Job) -> Job:
# Mirrors the real repository's idempotency on job_key.
existing = next((j for j in self.jobs if j.job_key == job.job_key), None)
if existing is not None:
return existing
self.jobs.append(job)
return job
def _source(source_id: UUID, kind: ConnectorKind = ConnectorKind.FOLDER) -> SourceRecord:
return SourceRecord(
id=source_id,
workspace_id=WORKSPACE_ID,
space_id=SPACE_ID,
name="Runbooks",
kind=kind,
status=SourceStatus.ACTIVE,
config={"root": "/mnt/runbooks", "trust_level": "trusted_internal"},
)
def _service(
registry: FakeRegistry, documents: FakeDocuments, blobs: FakeBlobs, jobs: RecordingJobs
) -> SourceSyncService:
return SourceSyncService(
registry=registry, # type: ignore[arg-type]
documents=documents, # type: ignore[arg-type]
blob_storage=blobs, # type: ignore[arg-type]
jobs=JobService(jobs), # type: ignore[arg-type]
)
@pytest.mark.asyncio
async def test_first_sync_ingests_every_item_as_an_ingest_upload_job() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "dir/b.md": b"bravo"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
report = await _service(registry, documents, blobs, jobs).run(
_source(source_id), connector, job_id=uuid4()
)
# Enqueued is not ingested: the run cannot honestly say "completed" while
# the child jobs that make the content searchable are still in flight.
assert report.run.outcome is SourceSyncOutcome.AWAITING_INGEST
assert (report.run.discovered, report.run.new, report.run.changed, report.run.unchanged) == (
2,
2,
0,
0,
)
assert report.run.removed == 0 and report.run.failed == 0
assert report.run.pending == 2
assert report.run.revision == "abc1234def"
assert len(jobs.jobs) == 2
for job in jobs.jobs:
assert job.operation == "ingest_upload"
assert job.payload["source_id"] == str(source_id)
assert job.payload["space_id"] == str(SPACE_ID)
assert job.payload["workspace_id"] == str(WORKSPACE_ID)
assert job.payload["trust_level"] == "trusted_internal"
assert job.payload["revision"] == "abc1234def"
assert job.payload["external_id"] in {"a.md", "dir/b.md"}
assert registry.states[-1] == (SourceSyncState.AWAITING_INGEST, False)
assert {record.external_id for record in registry.items.values()} == {"a.md", "dir/b.md"}
assert all(record.state is SourceItemState.PENDING for record in registry.items.values())
@pytest.mark.asyncio
async def test_resync_skips_unchanged_ingests_changed_and_retires_removed() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "b.md": b"bravo", "c.md": b"charlie"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
first_jobs = len(jobs.jobs)
retired_doc = documents.seed(source_id, "c.md")
connector.files["b.md"] = b"bravo v2"
del connector.files["c.md"]
connector.acquired.clear()
report = await service.run(_source(source_id), connector)
assert (
report.run.discovered,
report.run.new,
report.run.changed,
report.run.unchanged,
report.run.removed,
) == (2, 0, 1, 1, 1)
assert connector.acquired == ["b.md"], "unchanged content is never re-acquired"
assert len(jobs.jobs) == first_jobs + 1
assert documents.retired == [retired_doc.id]
assert report.retired_document_ids == (retired_doc.id,)
assert registry.items[(source_id, "c.md")].removed is True
assert registry.items[(source_id, "a.md")].removed is False
@pytest.mark.asyncio
async def test_multi_page_discovery_is_still_complete_and_reconciles_removals() -> None:
source_id = uuid4()
files = {f"doc{i:02d}.md": f"body {i}".encode() for i in range(7)}
connector = FakeConnector(source_id, files, page_size=3)
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
first = await service.run(_source(source_id), connector)
assert first.run.discovered == 7
removed = documents.seed(source_id, "doc03.md")
del connector.files["doc03.md"]
second = await service.run(_source(source_id), connector)
assert second.run.removed == 1
assert documents.retired == [removed.id]
@pytest.mark.asyncio
async def test_discovery_failure_records_a_failed_run_and_retires_nothing() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
documents.seed(source_id, "a.md")
connector.fail_scan = True
with pytest.raises(Exception, match="discovery failed"):
await service.run(_source(source_id), connector)
failed_runs = [run for run in registry.runs.values() if run.outcome is SourceSyncOutcome.FAILED]
assert len(failed_runs) == 1
assert failed_runs[0].error_code == "discovery_failed"
assert documents.retired == [], "an incomplete discovery never retires documents"
assert registry.states[-1] == (SourceSyncState.FAILED, False)
@pytest.mark.asyncio
async def test_upload_sources_are_not_syncable() -> None:
source_id = uuid4()
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
with pytest.raises(Exception, match="only connector-backed"):
await service.run(_source(source_id, ConnectorKind.UPLOAD), FakeConnector(source_id, {}))
@pytest.mark.asyncio
async def test_re_enqueueing_the_same_content_collapses_onto_one_job() -> None:
"""Same source item + same fingerprint -> same idempotency key: a sync
that re-sees content whose earlier job is still queued does not duplicate it."""
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
# Forget the registry memory (as if the first run's bookkeeping were lost)
# and sync again: the job key still dedups.
registry.items.clear()
await service.run(_source(source_id), connector)
assert len(jobs.jobs) == 1
assert jobs.jobs[0].available_at <= datetime.now(UTC)
async def _apply_all(
registry: FakeRegistry, source_id: UUID, *, only: set[str] | None = None
) -> None:
"""Stand in for the child ingest jobs reporting success."""
now = datetime.now(UTC)
for (sid, external_id), record in list(registry.items.items()):
if sid == source_id and not record.removed and (only is None or external_id in only):
await registry.mark_item_ingested(
source_id,
external_id,
fingerprint=record.fingerprint,
document_id=uuid4(),
now=now,
)
@pytest.mark.asyncio
async def test_a_source_only_completes_once_every_child_ingestion_applied() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "b.md": b"bravo"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
first = await service.run(_source(source_id), connector)
assert first.run.outcome is SourceSyncOutcome.AWAITING_INGEST
await _apply_all(registry, source_id)
second = await service.run(_source(source_id), connector)
assert second.run.outcome is SourceSyncOutcome.COMPLETED
assert (second.run.unchanged, second.run.pending, second.run.failed) == (2, 0, 0)
assert registry.states[-1] == (SourceSyncState.COMPLETED, True)
assert len(jobs.jobs) == 2, "nothing is re-enqueued once the content is applied"
@pytest.mark.asyncio
async def test_an_unchanged_item_keeps_the_fingerprint_its_ingestion_applied() -> None:
"""The regression that makes convergence meaningless: a re-sync that
rebuilds the remembered record loses ``applied_fingerprint`` and every
already-searchable item silently reverts to pending."""
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
await _apply_all(registry, source_id)
applied_at = registry.items[(source_id, "a.md")].applied_at
await service.run(_source(source_id), connector)
record = registry.items[(source_id, "a.md")]
assert record.applied is True
assert record.state is SourceItemState.APPLIED
assert record.applied_at == applied_at, "a no-op re-sync must not restamp convergence"
@pytest.mark.asyncio
async def test_a_failed_child_ingestion_settles_the_source_with_failures() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "b.md": b"bravo"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
# "a.md" became searchable; "b.md"'s ingestion failed for good.
await _apply_all(registry, source_id, only={"a.md"})
await registry.mark_item_failed(
source_id,
"b.md",
fingerprint=registry.items[(source_id, "b.md")].fingerprint,
failure_code="security_quarantined",
retryable=False,
now=datetime.now(UTC),
)
report = await service.run(_source(source_id), connector)
assert report.run.outcome is SourceSyncOutcome.COMPLETED_WITH_FAILURES
assert (report.run.failed, report.run.pending) == (1, 0)
assert report.run.error_code == "partial_failures"
assert registry.states[-1] == (SourceSyncState.COMPLETED_WITH_FAILURES, False)
assert registry.items[(source_id, "b.md")].state is SourceItemState.FAILED
assert len(jobs.jobs) == 2, (
"a terminally failed fingerprint is not re-enqueued: the child job for "
"that exact content already exists and will never report again"
)
@pytest.mark.asyncio
async def test_recovery_from_a_terminal_failure_runs_through_the_child_job_not_a_resync() -> None:
"""The full recovery loop for content that has not changed.
The chosen semantics are that a terminally failed item whose *discovered*
fingerprint is unchanged is never re-enqueued by a later sync -- the child
job for that exact content already exists, so re-enqueueing would either
collapse onto it or spin. That is a deliberate choice and it only holds up
if the failure is recoverable some other way, so this test walks the whole
path: fail -> the failure names its job -> sync again changes nothing ->
that job is retried -> the item converges.
Without the last two steps the "no re-enqueue" rule would be a permanent
dead end dressed up as a policy.
"""
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
first = await service.run(_source(source_id), connector)
ingest_job_id = first.enqueued_job_ids[0]
fingerprint = registry.items[(source_id, "a.md")].fingerprint
# The child ingestion fails terminally, naming the job that failed.
await registry.mark_item_failed(
source_id,
"a.md",
fingerprint=fingerprint,
failure_code="embedding_failed",
retryable=True,
now=datetime.now(UTC),
)
failed = registry.items[(source_id, "a.md")]
assert failed.state is SourceItemState.FAILED
assert failed.applied is False
# Syncing again is honest about it: nothing new is enqueued, the item is
# still failed, and the source does not claim to have completed cleanly.
again = await service.run(_source(source_id), connector)
assert len(jobs.jobs) == 1, "the same terminal fingerprint is never re-enqueued"
assert again.run.outcome is SourceSyncOutcome.COMPLETED_WITH_FAILURES
assert again.run.failed == 1
assert registry.items[(source_id, "a.md")].state is SourceItemState.FAILED
# Recovery: an authorized operator retries that child ingest job, and this
# time it reports success for the same fingerprint.
await registry.mark_item_ingested(
source_id,
"a.md",
fingerprint=fingerprint,
document_id=uuid4(),
now=datetime.now(UTC),
)
recovered = registry.items[(source_id, "a.md")]
assert recovered.applied_fingerprint == fingerprint
assert recovered.applied is True
assert recovered.state is SourceItemState.APPLIED
assert recovered.last_failure_code is None
summary = await registry.summarize_items(source_id)
assert (summary.failed, summary.pending) == (0, 0)
assert summary.converged is True
assert ingest_job_id is not None
@pytest.mark.asyncio
async def test_a_terminal_failure_remembers_the_job_an_operator_must_retry() -> None:
""""Retry the failed ingest job" needs an actual job to point at."""
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
report = await service.run(_source(source_id), connector)
job_id = report.enqueued_job_ids[0]
await registry.mark_item_failed(
source_id,
"a.md",
fingerprint=registry.items[(source_id, "a.md")].fingerprint,
failure_code="embedding_failed",
retryable=True,
now=datetime.now(UTC),
job_id=job_id,
)
assert registry.items[(source_id, "a.md")].last_failure_job_id == job_id
@pytest.mark.asyncio
async def test_changed_content_clears_a_previous_failure_and_keeps_what_was_applied() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
await _apply_all(registry, source_id)
applied = registry.items[(source_id, "a.md")].applied_fingerprint
connector.files["a.md"] = b"alpha v2"
report = await service.run(_source(source_id), connector)
record = registry.items[(source_id, "a.md")]
assert report.run.outcome is SourceSyncOutcome.AWAITING_INGEST
assert record.state is SourceItemState.PENDING
assert record.applied is False
assert record.applied_fingerprint == applied, (
"the old content stays searchable until the new content is really applied"
)
assert record.pending_job_id == report.enqueued_job_ids[0]
@pytest.mark.asyncio
async def test_a_retired_item_counts_as_converged_not_as_outstanding_work() -> None:
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "gone.md": b"gone"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
await _apply_all(registry, source_id)
del connector.files["gone.md"]
report = await service.run(_source(source_id), connector)
assert report.run.outcome is SourceSyncOutcome.COMPLETED
assert report.run.removed == 1
summary = await registry.summarize_items(source_id)
assert (summary.removed, summary.applied, summary.pending, summary.failed) == (1, 1, 0, 0)
assert summary.converged is True
@pytest.mark.asyncio
async def test_a_retired_item_that_reappears_unchanged_is_pending_again() -> None:
"""Retirement records "no searchable content" as the *applied* state. When
the file comes back -- even byte-identical -- that applied state is stale:
the document is still retired until the re-enqueued ingest reactivates it,
so carrying the retirement's fingerprint forward would report the source
converged while the reactivation is still in flight."""
source_id = uuid4()
connector = FakeConnector(source_id, {"a.md": b"alpha", "back.md": b"returns"})
registry, documents, blobs, jobs = FakeRegistry(), FakeDocuments(), FakeBlobs(), RecordingJobs()
service = _service(registry, documents, blobs, jobs)
await service.run(_source(source_id), connector)
await _apply_all(registry, source_id)
returning = dict(connector.files)
del connector.files["back.md"]
await service.run(_source(source_id), connector)
assert registry.items[(source_id, "back.md")].state is SourceItemState.REMOVED
connector.files = returning
report = await service.run(_source(source_id), connector)
record = registry.items[(source_id, "back.md")]
assert record.removed is False
assert record.state is SourceItemState.PENDING
assert record.applied is False
assert report.run.outcome is SourceSyncOutcome.AWAITING_INGEST