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,73 @@
"""TelemetryMiddleware: a real HTTP trace and request metric per request."""
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from ragcore.api.middleware import TelemetryMiddleware
from ragcore.platform.logging import RequestCorrelationMiddleware
from ragcore.platform.telemetry import TelemetryMetrics
def _app() -> tuple[FastAPI, InMemorySpanExporter, InMemoryMetricReader]:
span_exporter = InMemorySpanExporter()
tracer_provider = TracerProvider(resource=Resource.create({"service.name": "test"}))
tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
metric_reader = InMemoryMetricReader()
meter_provider = MeterProvider(
resource=Resource.create({"service.name": "test"}), metric_readers=[metric_reader]
)
metrics = TelemetryMetrics(meter_provider=meter_provider)
app = FastAPI()
@app.get("/ping")
async def ping() -> dict[str, str]:
return {"status": "ok"}
# Mirrors the real app's middleware order: TelemetryMiddleware must run
# after RequestCorrelationMiddleware to read the request_id it attaches.
app.add_middleware(TelemetryMiddleware, metrics=metrics, tracer_provider=tracer_provider)
app.add_middleware(RequestCorrelationMiddleware)
return app, span_exporter, metric_reader
def _metric_names(reader: InMemoryMetricReader) -> set[str]:
data = reader.get_metrics_data()
if data is None:
return set()
return {
metric.name
for resource_metric in data.resource_metrics
for scope_metric in resource_metric.scope_metrics
for metric in scope_metric.metrics
}
def test_a_request_produces_one_correlated_http_trace_span() -> None:
app, span_exporter, _ = _app()
with TestClient(app) as client:
response = client.get("/ping", headers={"X-Request-ID": "req-telemetry-1"})
assert response.status_code == 200
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
assert spans[0].name == "http_request"
assert spans[0].attributes["correlation_id"] == "req-telemetry-1"
assert spans[0].attributes["http.path"] == "/ping"
def test_a_request_produces_the_http_request_duration_metric() -> None:
app, _, metric_reader = _app()
with TestClient(app) as client:
client.get("/ping")
assert "ragcore_request_duration_seconds" in _metric_names(metric_reader)
@@ -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
+116
View File
@@ -0,0 +1,116 @@
"""Unit coverage for deterministic structure-aware chunking."""
from __future__ import annotations
from uuid import uuid4
from ragcore.domain.sources import SourceFingerprint
from ragcore.ingestion.chunking import ChunkProfile, StructureAwareChunker
from ragcore.ingestion.parsing import (
CanonicalArtifact,
CanonicalBlock,
CanonicalBlockKind,
ExtractionReport,
)
def artifact() -> CanonicalArtifact:
return CanonicalArtifact(
document_version_id=uuid4(),
source_fingerprint=SourceFingerprint("c" * 64),
blocks=(
CanonicalBlock(
CanonicalBlockKind.HEADING,
"/blocks/0000",
"Steps",
{"line_start": 1, "line_end": 1},
),
CanonicalBlock(
CanonicalBlockKind.PARAGRAPH,
"/blocks/0001",
"Do the thing.",
{"line_start": 2, "line_end": 2},
),
CanonicalBlock(
CanonicalBlockKind.HEADING,
"/blocks/0002",
"Code",
{"line_start": 4, "line_end": 4},
),
CanonicalBlock(
CanonicalBlockKind.CODE,
"/blocks/0003",
"def first():\n return 1\n\nclass Second:\n pass",
{"line_start": 5, "line_end": 9},
),
CanonicalBlock(
CanonicalBlockKind.TABLE,
"/blocks/0004",
"| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |",
{"line_start": 11, "line_end": 14},
),
),
extraction_report=ExtractionReport("text-parser", "1", "text-v1", False),
)
def test_same_canonical_input_and_profile_has_identical_ids() -> None:
source = artifact()
first = StructureAwareChunker().chunk(source, ChunkProfile.PROSE)
second = StructureAwareChunker().chunk(source, ChunkProfile.PROSE)
assert [chunk.id for chunk in first.chunks] == [chunk.id for chunk in second.chunks]
assert [chunk.content for chunk in first.chunks] == [
chunk.content for chunk in second.chunks
]
def test_section_symbol_and_table_boundaries_have_parent_links() -> None:
result = StructureAwareChunker().chunk(artifact(), ChunkProfile.CODE)
children = [chunk for chunk in result.chunks if chunk.parent_id is not None]
assert any("/symbol/0000" in chunk.structure_path for chunk in children)
assert any("/symbol/0001" in chunk.structure_path for chunk in children)
assert all(
chunk.parent_id
in {parent.id for parent in result.chunks if parent.parent_id is None}
for chunk in children
)
table_result = StructureAwareChunker().chunk(artifact(), ChunkProfile.TABLE)
table_children = [
chunk
for chunk in table_result.chunks
if chunk.source_structure_path == "/blocks/0004"
]
assert table_children
assert all("| A | B |" in chunk.content for chunk in table_children)
def test_token_maxima_and_profile_change_are_enforced_in_identity() -> None:
source = artifact()
long_block = CanonicalBlock(
CanonicalBlockKind.PARAGRAPH,
"/blocks/long",
" ".join(f"word{index}" for index in range(2405)),
{"line_start": 1, "line_end": 2405},
)
source = CanonicalArtifact(
source.document_version_id,
source.source_fingerprint,
(long_block,),
source.extraction_report,
)
prose = StructureAwareChunker().chunk(source, ChunkProfile.PROSE)
runbook = StructureAwareChunker().chunk(source, ChunkProfile.RUNBOOK)
assert all(chunk.token_count <= 1200 for chunk in prose.chunks)
assert all(chunk.token_count <= 1200 for chunk in runbook.chunks)
assert {chunk.id for chunk in prose.chunks}.isdisjoint(
{chunk.id for chunk in runbook.chunks}
)
assert all(
chunk.parent_id is not None
for chunk in prose.chunks
if chunk.metadata["role"] == "child"
)
@@ -0,0 +1,75 @@
"""Control-plane domain invariants."""
from __future__ import annotations
from uuid import uuid4
import pytest
from ragcore.domain.control import (
CrossWorkspaceRelationship,
InvalidEntity,
Role,
Scope,
Workspace,
)
def test_workspace_creates_typed_control_plane_entities() -> None:
workspace = Workspace(uuid4(), "Platform", "platform")
application = workspace.create_application(
id=uuid4(), name="Search client", scopes={Scope.SEARCH, Scope.DOCUMENTS_READ}
)
space = workspace.create_knowledge_space(
id=uuid4(), name="Runbooks", slug="runbooks", retention_days=30
)
service_account = application.create_service_account(
id=uuid4(), name="worker", scopes={Scope.SEARCH}
)
policy = workspace.create_policy(
id=uuid4(), operation="search", knowledge_space=space, required_scopes={Scope.SEARCH}
)
grant = workspace.grant(
id=uuid4(), application=application, knowledge_space=space, role=Role.VIEWER
)
assert application.workspace_id == workspace.id
assert service_account.application_id == application.id
assert service_account.workspace_id == workspace.id
assert policy.knowledge_space_id == space.id
assert grant.knowledge_space_id == space.id
def test_service_account_cannot_escalate_application_scopes() -> None:
workspace = Workspace(uuid4(), "Platform", "platform")
application = workspace.create_application(id=uuid4(), name="Search", scopes={Scope.SEARCH})
with pytest.raises(InvalidEntity, match="scopes"):
application.create_service_account(
id=uuid4(), name="overreach", scopes={Scope.SEARCH, Scope.ANSWER}
)
def test_cross_workspace_grant_is_rejected() -> None:
first = Workspace(uuid4(), "First", "first")
second = Workspace(uuid4(), "Second", "second")
application = first.create_application(id=uuid4(), name="Client", scopes={Scope.SEARCH})
space = second.create_knowledge_space(id=uuid4(), name="Private", slug="private")
with pytest.raises(CrossWorkspaceRelationship, match="same workspace"):
first.grant(id=uuid4(), application=application, knowledge_space=space, role=Role.VIEWER)
@pytest.mark.parametrize("slug", ["Upper", "with space", "-leading", "trailing-"])
def test_workspace_and_space_slugs_are_normalized_by_contract(slug: str) -> None:
with pytest.raises(InvalidEntity, match="slug"):
Workspace(uuid4(), "Invalid", slug)
def test_retention_policy_must_be_positive() -> None:
workspace = Workspace(uuid4(), "Platform", "platform")
with pytest.raises(InvalidEntity, match="retention_days"):
workspace.create_knowledge_space(
id=uuid4(), name="Runbooks", slug="runbooks", retention_days=0
)
@@ -0,0 +1,259 @@
"""ClaimCitationValidator: schema, citation and claim-grounding validation."""
from __future__ import annotations
import json
from datetime import UTC, datetime
from uuid import UUID, uuid4
import pytest
from ragcore.domain.citations import Citation
from ragcore.generation.validation import (
ClaimCitationValidator,
MalformedOutputError,
UnknownCitationError,
UnsupportedClaimError,
ValidationRetriesExhaustedError,
)
from ragcore.retrieval.context.models import AnswerabilityStatus, ContextBlock, ContextPackage
WORKSPACE_ID = UUID("00000000-0000-0000-0000-000000000001")
SPACE_ID = UUID("00000000-0000-0000-0000-000000000002")
RUN_ID = UUID("00000000-0000-0000-0000-000000000003")
def _citation(excerpt: str) -> Citation:
return Citation.create(
workspace_id=WORKSPACE_ID,
space_id=SPACE_ID,
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=uuid4(),
parent_chunk_id=None,
location={"page_start": 1},
excerpt=excerpt,
source_uri="https://example.test/doc.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=RUN_ID,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
def _package(*, excerpts: tuple[str, ...]) -> ContextPackage:
citations = [_citation(excerpt) for excerpt in excerpts]
blocks = tuple(
ContextBlock(
text=citation.excerpt,
citation_ids=(citation.id,),
token_count=max(1, len(citation.excerpt.split())),
document_id=citation.document_id,
chunk_id=citation.chunk_id,
)
for citation in citations
)
return ContextPackage(
retrieval_run_id=RUN_ID,
answerability=AnswerabilityStatus.ANSWERABLE,
answerability_reasons=("test_fixture",),
blocks=blocks,
citations=tuple(citations),
token_count=sum(block.token_count for block in blocks),
effective_space_ids=(SPACE_ID,),
)
EVIDENCE_EXCERPT = (
"The quarterly maintenance window for the payment gateway runs every "
"second Sunday from midnight to four in the morning Central European time."
)
def _raw(*, status: str, answer: str, citations: list[str]) -> str:
return json.dumps({"status": status, "answer": answer, "citations": citations})
def test_grounded_answer_with_known_citations_passes() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
citation_id = package.citations[0].id
raw_output = _raw(
status="answerable",
answer=(
"The quarterly maintenance window for the payment gateway runs "
"every second Sunday from midnight to four in the morning."
),
citations=[str(citation_id)],
)
validated = ClaimCitationValidator().parse_and_validate(raw_output, context=package)
assert validated.status is AnswerabilityStatus.ANSWERABLE
assert validated.citation_ids == (citation_id,)
def test_unknown_citation_is_rejected() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
raw_output = _raw(
status="answerable",
answer="The maintenance window runs every second Sunday.",
citations=[str(uuid4())],
)
with pytest.raises(UnknownCitationError):
ClaimCitationValidator().parse_and_validate(raw_output, context=package)
def test_forbidden_citation_outside_this_answers_evidence_set_is_rejected() -> None:
package_a = _package(excerpts=(EVIDENCE_EXCERPT,))
package_b = _package(excerpts=("unrelated other evidence text entirely",))
foreign_citation_id = package_b.citations[0].id
raw_output = _raw(
status="answerable",
answer="The maintenance window runs every second Sunday.",
citations=[str(foreign_citation_id)],
)
with pytest.raises(UnknownCitationError):
ClaimCitationValidator().parse_and_validate(raw_output, context=package_a)
def test_unsupported_claim_sentence_is_rejected() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
citation_id = package.citations[0].id
raw_output = _raw(
status="answerable",
answer=(
"The maintenance window runs every second Sunday. "
"The company headquarters relocated to Antarctica last year."
),
citations=[str(citation_id)],
)
with pytest.raises(UnsupportedClaimError, match="unsupported_claim"):
ClaimCitationValidator().parse_and_validate(raw_output, context=package)
def test_short_filler_sentences_are_never_flagged_as_claims() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
citation_id = package.citations[0].id
raw_output = _raw(
status="answerable",
answer=(
"Yes. Indeed. "
"The quarterly maintenance window for the payment gateway runs "
"every second Sunday from midnight to four in the morning."
),
citations=[str(citation_id)],
)
validated = ClaimCitationValidator().parse_and_validate(raw_output, context=package)
assert validated.status is AnswerabilityStatus.ANSWERABLE
def test_not_answerable_status_with_empty_answer_passes() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
raw_output = _raw(status="not_answerable", answer="", citations=[])
validated = ClaimCitationValidator().parse_and_validate(raw_output, context=package)
assert validated.status is AnswerabilityStatus.NOT_ANSWERABLE
assert validated.answer == ""
@pytest.mark.parametrize(
"raw_output",
[
pytest.param("not json at all", id="invalid_json"),
pytest.param("[]", id="json_array_not_object"),
pytest.param(json.dumps({"answer": "x", "citations": []}), id="missing_status"),
pytest.param(
json.dumps({"status": "banana", "answer": "x", "citations": []}),
id="invalid_status_value",
),
pytest.param(
json.dumps({"status": "answerable", "answer": 5, "citations": []}),
id="answer_wrong_type",
),
pytest.param(
json.dumps({"status": "answerable", "answer": "x", "citations": "not-a-list"}),
id="citations_wrong_type",
),
pytest.param(
json.dumps({"status": "answerable", "answer": "x", "citations": ["not-a-uuid"]}),
id="citation_not_a_uuid",
),
pytest.param("", id="empty_output"),
],
)
def test_malformed_output_is_rejected(raw_output: str) -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
with pytest.raises(MalformedOutputError):
ClaimCitationValidator().parse_and_validate(raw_output, context=package)
async def test_validate_with_retries_succeeds_after_a_malformed_first_attempt() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
citation_id = package.citations[0].id
attempts = {"count": 0}
async def generate() -> str:
attempts["count"] += 1
if attempts["count"] == 1:
return "not json"
return _raw(
status="answerable",
answer="The maintenance window runs every second Sunday.",
citations=[str(citation_id)],
)
validated = await ClaimCitationValidator().validate_with_retries(
generate=generate, context=package, max_retries=2
)
assert validated.attempt == 2
assert attempts["count"] == 2
async def test_validate_with_retries_exhausts_and_fails_stable() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
attempts = {"count": 0}
async def generate() -> str:
attempts["count"] += 1
return "still not json"
with pytest.raises(ValidationRetriesExhaustedError) as excinfo:
await ClaimCitationValidator().validate_with_retries(
generate=generate, context=package, max_retries=1
)
assert excinfo.value.last_code == "malformed_output"
# Exactly max_retries + 1 attempts -- bounded, never an unbounded retry loop.
assert attempts["count"] == 2
async def test_validate_with_retries_bounds_calls_even_for_content_failures() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
async def generate() -> str:
return _raw(
status="answerable",
answer="The company headquarters relocated to Antarctica last year.",
citations=[],
)
with pytest.raises(ValidationRetriesExhaustedError) as excinfo:
await ClaimCitationValidator().validate_with_retries(
generate=generate, context=package, max_retries=0
)
assert excinfo.value.last_code == "unsupported_claim"
def test_min_overlap_ratio_out_of_range_is_rejected() -> None:
with pytest.raises(ValueError, match="min_overlap_ratio"):
ClaimCitationValidator(min_overlap_ratio=0.0)
with pytest.raises(ValueError, match="min_overlap_ratio"):
ClaimCitationValidator(min_overlap_ratio=1.5)
async def test_negative_max_retries_is_rejected() -> None:
package = _package(excerpts=(EVIDENCE_EXCERPT,))
async def generate() -> str:
return "irrelevant"
with pytest.raises(ValueError, match="max_retries"):
await ClaimCitationValidator().validate_with_retries(
generate=generate, context=package, max_retries=-1
)
+256
View File
@@ -0,0 +1,256 @@
from __future__ import annotations
from collections.abc import AsyncIterator, Sequence
from dataclasses import replace
from uuid import UUID, uuid4
import pytest
from ragcore.application.indexes.models import (
IndexBuildError,
MalformedIndexPoint,
StoredIndexPoint,
index_point_fingerprint,
)
from ragcore.application.indexes.shadow_migration import (
ShadowEmbeddingMigrationService,
ShadowMigrationCheckpoint,
ShadowMigrationStatus,
ShadowPreflight,
)
from ragcore.infrastructure.qdrant.models import (
CollectionKind,
CollectionProfile,
IndexPayload,
IndexPoint,
)
class MemoryCheckpoints:
def __init__(self, *, cancel_after_batch: bool = False) -> None:
self.item: ShadowMigrationCheckpoint | None = None
self.cancel_after_batch = cancel_after_batch
async def load(self, _migration_id: UUID) -> ShadowMigrationCheckpoint | None:
return self.item
async def save(self, checkpoint: ShadowMigrationCheckpoint) -> None:
if self.cancel_after_batch and checkpoint.request_batches == 1:
checkpoint = replace(checkpoint, cancel_requested=True)
self.item = checkpoint
class Source:
def __init__(self, points: Sequence[IndexPoint]) -> None:
self.points = points
async def iter_points(self, _profile: CollectionProfile) -> AsyncIterator[IndexPoint]:
for point in self.points:
yield point
class Store:
def __init__(self, *, fail_once: bool = False, omit_last: bool = False) -> None:
self.points: dict[UUID, StoredIndexPoint] = {}
self.fail_once = fail_once
self.omit_last = omit_last
self.alias = "rag_dense_legacy_v1"
self.alias_mutations = 0
self.upsert_calls = 0
async def create_shadow(self, _profile: CollectionProfile) -> None:
return None
async def delete_shadow(self, _profile: CollectionProfile) -> None:
raise AssertionError("validated shadow is retained")
async def upsert_points(
self, _profile: CollectionProfile, points: Sequence[IndexPoint]
) -> None:
self.upsert_calls += 1
if self.fail_once:
self.fail_once = False
raise TimeoutError("transient")
for point in points:
self.points[point.point_id] = StoredIndexPoint(
point.point_id, index_point_fingerprint(point)
)
async def delete_points(self, _profile: CollectionProfile, _point_ids: Sequence[UUID]) -> None:
raise AssertionError("no destructive cleanup")
async def delete_raw_points(self, _profile: CollectionProfile, _raw_ids: Sequence[str]) -> None:
raise AssertionError("no destructive cleanup")
async def count_points(self, _profile: CollectionProfile) -> int:
return len(self.points)
async def list_points(
self, _profile: CollectionProfile
) -> tuple[tuple[StoredIndexPoint, ...], tuple[MalformedIndexPoint, ...]]:
values = tuple(self.points.values())
return (values[:-1] if self.omit_last and values else values), ()
async def current_alias_target(self, _alias_name: str) -> str:
return self.alias
async def activate(self, _profile: CollectionProfile) -> None:
self.alias_mutations += 1
async def rollback(self, _alias_name: str, _previous_collection: str | None) -> None:
self.alias_mutations += 1
def profile() -> CollectionProfile:
return CollectionProfile(
kind=CollectionKind.DENSE,
profile="modelforge-shadow",
version="v1",
dimensions=3,
model_digest="a" * 64,
embedding_space_id=uuid4(),
)
def points(item: CollectionProfile, count: int = 3) -> tuple[IndexPoint, ...]:
workspace, space, document, version, source = (uuid4() for _ in range(5))
return tuple(
IndexPoint(
point_id=(chunk := uuid4()),
dense_vector=(1.0, 0.0, 0.0),
payload=IndexPayload(
workspace_id=workspace,
space_id=space,
document_id=document,
document_version_id=version,
chunk_id=chunk,
parent_chunk_id=None,
source_id=source,
status="published",
content_type="text/plain",
language="en",
trust_level="internal",
location={"structure_path": f"/{index}"},
parser_version="parser-v1",
chunk_profile_version=item.version,
embedding_model_digest=item.model_digest,
text=f"chunk {index}",
excerpt=f"chunk {index}",
embedding_space_id=item.embedding_space_id,
),
)
for index in range(count)
)
def checkpoint(item: CollectionProfile, total: int = 3) -> ShadowMigrationCheckpoint:
assert item.embedding_space_id is not None
return ShadowMigrationCheckpoint(
migration_id=uuid4(),
source_index="rag_dense_legacy_v1",
target_index=item.collection_name,
source_embedding_space="legacy-observed",
target_embedding_space_id=item.embedding_space_id,
corpus_revision="corpus-sha256",
total_chunks=total,
)
async def preflight(*, passed: bool = True) -> ShadowPreflight:
return ShadowPreflight(
production_healthy=passed,
source_available=True,
modelforge_healthy=True,
credential_valid=True,
embedding_space_matches=True,
dimension_matches=True,
distance_metric_matches=True,
qdrant_reachable=True,
storage_sufficient=True,
target_conflict_free=True,
)
@pytest.mark.asyncio
async def test_preflight_failure_writes_no_vectors() -> None:
item = profile()
store, state = Store(), MemoryCheckpoints()
service = ShadowEmbeddingMigrationService(store, state)
with pytest.raises(IndexBuildError, match="preflight"):
await service.run(
checkpoint(item), item, Source(points(item)), lambda: preflight(passed=False)
)
assert not store.points
assert state.item is not None and state.item.status is ShadowMigrationStatus.FAILED
@pytest.mark.asyncio
async def test_backfill_is_bounded_validated_and_never_mutates_source_alias() -> None:
item, store, state = profile(), Store(), MemoryCheckpoints()
service = ShadowEmbeddingMigrationService(store, state, batch_size=2)
result = await service.run(checkpoint(item), item, Source(points(item)), preflight)
assert result.status is ShadowMigrationStatus.READY_FOR_EVALUATION
assert result.completed_chunks == 3 and result.failed_chunks == 0
assert result.request_batches == 2
assert result.validation["passed"] is True
assert result.validation["source_alias_unchanged"] is True
assert store.alias_mutations == 0
@pytest.mark.asyncio
async def test_resume_skips_integrity_valid_existing_chunk_and_is_idempotent() -> None:
item, store, states = profile(), Store(), MemoryCheckpoints()
source_points = points(item)
existing = source_points[0]
store.points[existing.point_id] = StoredIndexPoint(
existing.point_id, index_point_fingerprint(existing)
)
initial = replace(
checkpoint(item), status=ShadowMigrationStatus.BACKFILLING, completed_chunks=1
)
states.item = initial
service = ShadowEmbeddingMigrationService(store, states, batch_size=1)
result = await service.run(initial, item, Source(source_points), preflight)
assert result.completed_chunks == 3
assert store.upsert_calls == 2
repeated = await service.run(initial, item, Source(source_points), preflight)
assert repeated.status is ShadowMigrationStatus.READY_FOR_EVALUATION
assert store.upsert_calls == 2
@pytest.mark.asyncio
async def test_cancel_finishes_current_batch_and_remains_resumable() -> None:
item, store, states = profile(), Store(), MemoryCheckpoints(cancel_after_batch=True)
source_points = points(item)
service = ShadowEmbeddingMigrationService(store, states, batch_size=1)
cancelled = await service.run(checkpoint(item), item, Source(source_points), preflight)
assert cancelled.status is ShadowMigrationStatus.CANCELLED
assert cancelled.completed_chunks == 1
states.cancel_after_batch = False
resumed = await service.run(cancelled, item, Source(source_points), preflight)
assert resumed.status is ShadowMigrationStatus.READY_FOR_EVALUATION
assert resumed.completed_chunks == 3
@pytest.mark.asyncio
async def test_transient_retry_is_bounded_and_counted() -> None:
item, store, states = profile(), Store(fail_once=True), MemoryCheckpoints()
service = ShadowEmbeddingMigrationService(
store,
states,
batch_size=3,
max_retries=1,
retryable=lambda error: isinstance(error, TimeoutError),
)
result = await service.run(checkpoint(item), item, Source(points(item)), preflight)
assert result.retried_chunks == 3
assert store.upsert_calls == 2
@pytest.mark.asyncio
async def test_missing_point_blocks_evaluation_readiness() -> None:
item, store, states = profile(), Store(omit_last=True), MemoryCheckpoints()
service = ShadowEmbeddingMigrationService(store, states, batch_size=3)
with pytest.raises(IndexBuildError, match="validation"):
await service.run(checkpoint(item), item, Source(points(item)), preflight)
assert states.item is not None and states.item.status is ShadowMigrationStatus.FAILED
@@ -0,0 +1,109 @@
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
import httpx
import pytest
from ragcore.application.models.selection import EmbeddingCutoverRejected
from ragcore.config import Settings
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.selection_verifier import ModelForgeRouteSelectionVerifier
def _profile() -> EmbeddingProfile:
return EmbeddingProfile(
id=uuid4(),
profile="route-check",
version="v1",
provider=EmbeddingProvider.MODELFORGE,
model_identifier="rag.embedding@1",
model_digest="a" * 64,
dimensions=2,
context_limit=4096,
tokenizer="capability-managed",
data_policy=DataPolicy.LOCAL_ONLY,
embedding_space_id=uuid4(),
)
def _factory(client: httpx.AsyncClient, credential_file: Path):
return create_embedding_adapter_factory(
settings=Settings(
env="test",
modelforge_base_url="http://modelforge:8000",
modelforge_credential_file=credential_file,
modelforge_embedding_experiment_route="stale-global-route",
),
http_client=client,
)
def _write_credential(path: Path) -> None:
path.write_text("mf_" + "x" * 48, encoding="utf-8")
path.chmod(0o600)
@pytest.mark.asyncio
async def test_selection_probe_rejects_stale_route_embedding_space(tmp_path: Path) -> None:
profile = _profile()
selection = EmbeddingSelection.from_profile(profile, experiment_route_key="lab-route-current")
credential_file = tmp_path / "credential"
_write_credential(credential_file)
paths: list[str] = []
def respond(request: httpx.Request) -> httpx.Response:
paths.append(request.url.path)
return httpx.Response(
200,
json={
"capability": "rag.embedding@1",
"dimension": profile.dimensions,
"normalized": True,
"embedding_space_id": str(uuid4()),
"data": [[1.0, 0.0]],
"request_id": str(uuid4()),
},
)
async with httpx.AsyncClient(
transport=httpx.MockTransport(respond), follow_redirects=False
) as client:
verifier = ModelForgeRouteSelectionVerifier(_factory(client, credential_file))
with pytest.raises(EmbeddingCutoverRejected, match="identity probe"):
await verifier.verify(selection, profile)
assert paths == ["/api/v1/capability-experiments/lab-route-current/invoke"]
@pytest.mark.asyncio
async def test_selection_probe_records_gateway_request_identity(tmp_path: Path) -> None:
profile = _profile()
selection = EmbeddingSelection.from_profile(profile, experiment_route_key="lab-route-next")
credential_file = tmp_path / "credential"
_write_credential(credential_file)
request_id = uuid4()
def respond(_request: httpx.Request) -> httpx.Response:
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(request_id),
},
)
async with httpx.AsyncClient(
transport=httpx.MockTransport(respond), follow_redirects=False
) as client:
verifier = ModelForgeRouteSelectionVerifier(_factory(client, credential_file))
result = await verifier.verify(selection, profile)
assert result.gateway_request_ids == (request_id,)
@@ -0,0 +1,96 @@
from __future__ import annotations
from types import SimpleNamespace
from uuid import uuid4
import pytest
from qdrant_client.http import models
from ragcore.application.models.selection import EmbeddingCutoverRejected
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import EmbeddingProfile, EmbeddingProvider, EmbeddingSelection
from ragcore.infrastructure.qdrant.selection_verifier import QdrantEmbeddingSelectionVerifier
def _profile() -> EmbeddingProfile:
return EmbeddingProfile(
id=uuid4(),
profile="selected-index",
version="v2",
provider=EmbeddingProvider.MODELFORGE,
model_identifier="rag.embedding@1",
model_digest="b" * 64,
dimensions=3,
context_limit=4096,
tokenizer="capability-managed",
data_policy=DataPolicy.LOCAL_ONLY,
embedding_space_id=uuid4(),
)
class FakeQdrant:
def __init__(self, *, dimension: int, payloads: list[dict[str, object]]) -> None:
self.dimension = dimension
self.payloads = payloads
self.collections: list[str] = []
async def get_collection(self, collection_name: str) -> SimpleNamespace:
self.collections.append(collection_name)
return SimpleNamespace(
config=SimpleNamespace(
params=SimpleNamespace(
vectors={
"dense": models.VectorParams(
size=self.dimension, distance=models.Distance.COSINE
)
},
sparse_vectors={"sparse": models.SparseVectorParams()},
)
)
)
async def scroll(self, **_kwargs: object) -> tuple[list[SimpleNamespace], None]:
return [SimpleNamespace(payload=value) for value in self.payloads], None
def _payload(profile: EmbeddingProfile) -> dict[str, object]:
return {
"embedding_model_digest": profile.model_digest,
"chunk_profile_version": profile.version,
"embedding_space_id": str(profile.embedding_space_id),
}
@pytest.mark.asyncio
async def test_collection_verifier_rejects_a_stale_vector_dimension() -> None:
profile = _profile()
selection = EmbeddingSelection.from_profile(profile, experiment_route_key="lab-route-next")
client = FakeQdrant(dimension=2, payloads=[_payload(profile)])
with pytest.raises(EmbeddingCutoverRejected, match="vector shape"):
await QdrantEmbeddingSelectionVerifier(client).verify(selection, profile) # type: ignore[arg-type]
assert client.collections == ["rag_dense_selected-index_v2"]
@pytest.mark.asyncio
async def test_collection_verifier_rejects_stale_profile_identity_pins() -> None:
profile = _profile()
selection = EmbeddingSelection.from_profile(profile, experiment_route_key="lab-route-next")
stale = {**_payload(profile), "embedding_space_id": str(uuid4())}
client = FakeQdrant(dimension=3, payloads=[stale])
with pytest.raises(EmbeddingCutoverRejected, match="embedding-space pin"):
await QdrantEmbeddingSelectionVerifier(client).verify(selection, profile) # type: ignore[arg-type]
@pytest.mark.asyncio
async def test_collection_verifier_counts_only_the_exact_pinned_collection() -> None:
profile = _profile()
selection = EmbeddingSelection.from_profile(profile)
client = FakeQdrant(dimension=3, payloads=[_payload(profile), _payload(profile)])
result = await QdrantEmbeddingSelectionVerifier(client).verify(selection, profile) # type: ignore[arg-type]
assert client.collections == ["rag_dense_selected-index_v2"]
assert result.verified_point_count == 2
@@ -0,0 +1,91 @@
from __future__ import annotations
from types import SimpleNamespace
from uuid import uuid4
import httpx
import pytest
from qdrant_client.http import models
from qdrant_client.http.exceptions import UnexpectedResponse
from ragcore.application.indexes.models import IndexBuildError
from ragcore.infrastructure.qdrant.adapter import CollectionProfileMismatchError
from ragcore.infrastructure.qdrant.models import CollectionKind, CollectionProfile
from ragcore.infrastructure.qdrant.rebuild import QdrantRebuildStore
class MissingQdrantCollection:
def __init__(self) -> None:
self.reads = 0
self.creates = 0
async def get_collection(self, _name: str) -> object:
self.reads += 1
raise UnexpectedResponse(404, "Not Found", b"missing", httpx.Headers())
async def create_collection(self, **_kwargs: object) -> None:
self.creates += 1
@pytest.mark.asyncio
async def test_selected_collection_verification_fails_without_recreating_missing_collection() -> (
None
):
client = MissingQdrantCollection()
profile = CollectionProfile(
kind=CollectionKind.DENSE,
profile="selected-profile",
version="v2",
dimensions=4,
model_digest="a" * 64,
embedding_space_id=uuid4(),
)
with pytest.raises(CollectionProfileMismatchError, match="refusing to recreate"):
await QdrantRebuildStore(client).verify_existing_selection(profile) # type: ignore[arg-type]
assert client.reads == 1
assert client.creates == 0
@pytest.mark.asyncio
async def test_selected_collection_verification_checks_every_existing_point_pin() -> None:
space_id = uuid4()
class StalePinnedCollection:
async def get_collection(self, _name: str) -> object:
return SimpleNamespace(
config=SimpleNamespace(
params=SimpleNamespace(
vectors={
"dense": models.VectorParams(size=4, distance=models.Distance.COSINE)
},
sparse_vectors={"sparse": models.SparseVectorParams()},
)
)
)
async def scroll(self, **_kwargs: object) -> tuple[list[SimpleNamespace], None]:
return [
SimpleNamespace(
payload={
"embedding_model_digest": "b" * 64,
"chunk_profile_version": "v2",
"embedding_space_id": str(space_id),
}
)
], None
profile = CollectionProfile(
kind=CollectionKind.DENSE,
profile="selected-profile",
version="v2",
dimensions=4,
model_digest="a" * 64,
embedding_space_id=space_id,
)
with pytest.raises(IndexBuildError, match="incompatible identity pins"):
await QdrantRebuildStore(StalePinnedCollection()).verify_existing_selection( # type: ignore[arg-type]
profile
)
+52
View File
@@ -0,0 +1,52 @@
from __future__ import annotations
from ragcore.ingestion.language import (
detect_language,
normalize_language_tag,
resolve_document_language,
)
_DUTCH = (
"Dit is een handleiding voor het herstarten van de dienst. Controleer eerst "
"of de gateway niet bereikbaar is en wacht daarna tien seconden voordat de "
"schakelaar opnieuw wordt omgezet. Het systeem moet daarna weer normaal werken."
)
_ENGLISH = (
"This is the runbook for restarting the service. First confirm that the "
"gateway is not reachable and then wait ten seconds before you cycle the "
"breaker again. The system should be healthy after that and the alert clears."
)
_FRENCH = (
"Ce document décrit la procédure pour redémarrer le service. Vérifiez d'abord "
"que la passerelle n'est pas joignable et attendez ensuite dix secondes avant "
"de basculer le disjoncteur. Le système doit être de nouveau disponible."
)
_GERMAN = (
"Dieses Dokument beschreibt das Neustarten des Dienstes. Prüfen Sie zuerst, "
"ob das Gateway nicht erreichbar ist, und warten Sie danach zehn Sekunden, "
"bevor der Schalter wieder umgelegt wird. Das System muss danach laufen."
)
def test_detects_each_supported_language() -> None:
assert detect_language(_DUTCH) == "nl"
assert detect_language(_ENGLISH) == "en"
assert detect_language(_FRENCH) == "fr"
assert detect_language(_GERMAN) == "de"
def test_short_or_codeish_content_stays_undetected() -> None:
assert detect_language("restart") is None
assert detect_language("SELECT id FROM documents WHERE space_id = $1;") is None
def test_normalize_language_tag_reduces_locales() -> None:
assert normalize_language_tag("nl-BE") == "nl"
assert normalize_language_tag("EN_us") == "en"
assert normalize_language_tag("not a tag!") is None
def test_resolution_priority_is_explicit_then_detected_then_fallback() -> None:
assert resolve_document_language(explicit_locale="fr-FR", text=_DUTCH) == "fr"
assert resolve_document_language(explicit_locale=None, text=_DUTCH) == "nl"
assert resolve_document_language(explicit_locale="??", text="x") == "en"
@@ -0,0 +1,110 @@
from __future__ import annotations
import json
from uuid import UUID, uuid4
import httpx
import pytest
from ragcore.application.models import GenerationProfileRegistry
from ragcore.domain.control.entities import DataPolicy
from ragcore.domain.models import GenerationProfile, GenerationProvider
from ragcore.infrastructure.models.answer_generator import (
AnswerGenerationUnavailableError,
OllamaAnswerGenerator,
)
class InMemoryGenerationProfileStore:
def __init__(self) -> None:
self._profiles: dict[UUID, GenerationProfile] = {}
self._active_id: UUID | None = None
async def add(self, profile: GenerationProfile) -> None:
self._profiles[profile.id] = profile
async def get(self, profile_id: UUID) -> GenerationProfile | None:
return self._profiles.get(profile_id)
async def activate(self, profile_id: UUID) -> None:
self._active_id = profile_id
async def get_active(self) -> GenerationProfile | None:
return self._profiles.get(self._active_id) if self._active_id else None
def _profile(**overrides: object) -> GenerationProfile:
values: dict[str, object] = {
"id": uuid4(),
"profile": "answer-hybrid",
"version": "v1",
"provider": GenerationProvider.OLLAMA,
"model_identifier": "llama3.1:8b-instruct-q4_0",
"model_digest": "a" * 64,
"context_limit": 4096,
"max_output_tokens": 512,
"temperature": 0.2,
"data_policy": DataPolicy.LOCAL_ONLY,
"timeout_seconds": 5.0,
"max_retries": 0,
}
values.update(overrides)
return GenerationProfile(**values)
def _ndjson(*lines: dict[str, object]) -> bytes:
return b"\n".join(json.dumps(line).encode() for line in lines)
def _client(handler) -> httpx.AsyncClient: # type: ignore[no-untyped-def]
return httpx.AsyncClient(transport=httpx.MockTransport(handler), follow_redirects=False)
@pytest.mark.asyncio
async def test_generates_text_from_the_active_profile() -> None:
store = InMemoryGenerationProfileStore()
registry = GenerationProfileRegistry(store)
profile = await registry.register(_profile())
await registry.activate(profile.id)
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
content=_ndjson(
{"response": "Restart the ", "done": False},
{"response": "payment service.", "done": True},
),
)
generator = OllamaAnswerGenerator(registry, _client(handler), base_url="http://ollama")
text = await generator.generate(
prompt="How do I restart it?", generation_profile="anything", spaces=()
)
assert text == "Restart the payment service."
@pytest.mark.asyncio
async def test_raises_when_no_active_profile() -> None:
registry = GenerationProfileRegistry(InMemoryGenerationProfileStore())
def handler(request: httpx.Request) -> httpx.Response:
raise AssertionError("must not call Ollama without an active profile")
generator = OllamaAnswerGenerator(registry, _client(handler), base_url="http://ollama")
with pytest.raises(AnswerGenerationUnavailableError):
await generator.generate(prompt="anything", generation_profile="anything", spaces=())
@pytest.mark.asyncio
async def test_raises_when_the_provider_rejects_the_request() -> None:
store = InMemoryGenerationProfileStore()
registry = GenerationProfileRegistry(store)
profile = await registry.register(_profile())
await registry.activate(profile.id)
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(500, json={"error": "model not found"})
generator = OllamaAnswerGenerator(registry, _client(handler), base_url="http://ollama")
with pytest.raises(AnswerGenerationUnavailableError):
await generator.generate(prompt="anything", generation_profile="anything", spaces=())
@@ -0,0 +1,131 @@
from __future__ import annotations
from uuid import UUID, uuid4
import pytest
from ragcore.domain.control.entities import DataPolicy
from ragcore.domain.models import GenerationProfile, GenerationProfileError, GenerationProvider
def _profile(**overrides: object) -> GenerationProfile:
values: dict[str, object] = {
"id": uuid4(),
"profile": "answer-hybrid",
"version": "v1",
"provider": GenerationProvider.OLLAMA,
"model_identifier": "llama3.1:8b-instruct-q4_0",
"model_digest": "a" * 64,
"context_limit": 8192,
"max_output_tokens": 1024,
"temperature": 0.4,
"data_policy": DataPolicy.LOCAL_ONLY,
}
values.update(overrides)
return GenerationProfile(**values)
def test_valid_profile_is_accepted() -> None:
profile = _profile()
assert profile.provider is GenerationProvider.OLLAMA
assert profile.data_policy is DataPolicy.LOCAL_ONLY
def test_profile_rejects_nil_id() -> None:
with pytest.raises(GenerationProfileError):
_profile(id=UUID(int=0))
@pytest.mark.parametrize("bad_id", ["not a safe id!", "Also-Not-Safe", ""])
def test_profile_rejects_unsafe_profile_or_version(bad_id: str) -> None:
with pytest.raises(GenerationProfileError):
_profile(profile=bad_id)
with pytest.raises(GenerationProfileError):
_profile(version=bad_id)
def test_profile_rejects_floating_model_tags() -> None:
with pytest.raises(GenerationProfileError):
_profile(model_identifier="llama3.1:latest")
with pytest.raises(GenerationProfileError):
_profile(model_identifier="llama3.1:stable")
def test_profile_requires_a_pinned_sha256_digest() -> None:
with pytest.raises(GenerationProfileError):
_profile(model_digest="not-a-digest")
def test_max_output_tokens_must_not_exceed_context_limit() -> None:
with pytest.raises(GenerationProfileError):
_profile(context_limit=100, max_output_tokens=200)
@pytest.mark.parametrize("temperature", [-0.1, 2.1, float("nan"), float("inf")])
def test_temperature_must_be_within_bounds(temperature: float) -> None:
with pytest.raises(GenerationProfileError):
_profile(temperature=temperature)
@pytest.mark.parametrize("policy", [DataPolicy.RETRIEVAL_ONLY, DataPolicy.NO_GENERATION])
def test_profile_rejects_space_only_data_policies(policy: DataPolicy) -> None:
with pytest.raises(GenerationProfileError):
_profile(data_policy=policy)
@pytest.mark.parametrize(
"policy", [DataPolicy.LOCAL_ONLY, DataPolicy.LOCAL_PREFERRED, DataPolicy.CLOUD_ALLOWED]
)
def test_profile_accepts_generation_eligible_data_policies(policy: DataPolicy) -> None:
profile = _profile(data_policy=policy)
assert profile.data_policy is policy
def test_resolve_data_policy_never_broadens_beyond_the_profile() -> None:
profile = _profile(data_policy=DataPolicy.LOCAL_ONLY)
assert profile.resolve_data_policy(DataPolicy.CLOUD_ALLOWED) is DataPolicy.LOCAL_ONLY
assert profile.resolve_data_policy(DataPolicy.LOCAL_PREFERRED) is DataPolicy.LOCAL_ONLY
assert profile.resolve_data_policy(DataPolicy.LOCAL_ONLY) is DataPolicy.LOCAL_ONLY
assert profile.resolve_data_policy(None) is DataPolicy.LOCAL_ONLY
def test_resolve_data_policy_can_narrow_a_looser_profile() -> None:
profile = _profile(data_policy=DataPolicy.CLOUD_ALLOWED)
assert profile.resolve_data_policy(DataPolicy.LOCAL_ONLY) is DataPolicy.LOCAL_ONLY
assert profile.resolve_data_policy(None) is DataPolicy.CLOUD_ALLOWED
def test_resolve_data_policy_rejects_a_non_generation_request() -> None:
profile = _profile()
with pytest.raises(GenerationProfileError):
profile.resolve_data_policy(DataPolicy.NO_GENERATION)
def test_resolve_temperature_never_exceeds_the_profile_ceiling() -> None:
profile = _profile(temperature=0.4)
assert profile.resolve_temperature(1.5) == pytest.approx(0.4)
assert profile.resolve_temperature(0.1) == pytest.approx(0.1)
assert profile.resolve_temperature(None) == pytest.approx(0.4)
def test_resolve_temperature_rejects_invalid_requests() -> None:
profile = _profile()
with pytest.raises(GenerationProfileError):
profile.resolve_temperature(-1.0)
with pytest.raises(GenerationProfileError):
profile.resolve_temperature(float("nan"))
def test_resolve_max_output_tokens_never_exceeds_the_profile_cap() -> None:
profile = _profile(max_output_tokens=1024)
assert profile.resolve_max_output_tokens(4096) == 1024
assert profile.resolve_max_output_tokens(256) == 256
assert profile.resolve_max_output_tokens(None) == 1024
def test_resolve_max_output_tokens_rejects_invalid_requests() -> None:
profile = _profile()
with pytest.raises(GenerationProfileError):
profile.resolve_max_output_tokens(0)
with pytest.raises(GenerationProfileError):
profile.resolve_max_output_tokens(-5)
@@ -0,0 +1,97 @@
from __future__ import annotations
from uuid import UUID, uuid4
import pytest
from ragcore.application.models import (
DuplicateGenerationProfileError,
GenerationProfileNotFoundError,
GenerationProfileRegistry,
)
from ragcore.domain.control.entities import DataPolicy
from ragcore.domain.models import GenerationProfile, GenerationProvider
class InMemoryGenerationProfileStore:
def __init__(self) -> None:
self._profiles: dict[UUID, GenerationProfile] = {}
self._active_id: UUID | None = None
async def add(self, profile: GenerationProfile) -> None:
self._profiles[profile.id] = profile
async def get(self, profile_id: UUID) -> GenerationProfile | None:
return self._profiles.get(profile_id)
async def activate(self, profile_id: UUID) -> None:
self._active_id = profile_id
async def get_active(self) -> GenerationProfile | None:
return self._profiles.get(self._active_id) if self._active_id else None
def _profile(**overrides: object) -> GenerationProfile:
values: dict[str, object] = {
"id": uuid4(),
"profile": "answer-hybrid",
"version": "v1",
"provider": GenerationProvider.OLLAMA,
"model_identifier": "llama3.1:8b-instruct-q4_0",
"model_digest": "a" * 64,
"context_limit": 8192,
"max_output_tokens": 1024,
"temperature": 0.4,
"data_policy": DataPolicy.LOCAL_ONLY,
}
values.update(overrides)
return GenerationProfile(**values)
@pytest.mark.asyncio
async def test_register_persists_a_new_profile() -> None:
registry = GenerationProfileRegistry(InMemoryGenerationProfileStore())
profile = _profile()
registered = await registry.register(profile)
assert registered.id == profile.id
@pytest.mark.asyncio
async def test_register_rejects_a_duplicate_id() -> None:
store = InMemoryGenerationProfileStore()
registry = GenerationProfileRegistry(store)
profile = _profile()
await registry.register(profile)
with pytest.raises(DuplicateGenerationProfileError):
await registry.register(profile)
@pytest.mark.asyncio
async def test_activate_requires_a_registered_profile() -> None:
registry = GenerationProfileRegistry(InMemoryGenerationProfileStore())
with pytest.raises(GenerationProfileNotFoundError):
await registry.activate(uuid4())
@pytest.mark.asyncio
async def test_active_requires_something_activated_first() -> None:
store = InMemoryGenerationProfileStore()
registry = GenerationProfileRegistry(store)
profile = await registry.register(_profile())
with pytest.raises(GenerationProfileNotFoundError):
await registry.active()
activated = await registry.activate(profile.id)
assert activated.id == profile.id
assert (await registry.active()).id == profile.id
@pytest.mark.asyncio
async def test_activating_a_second_profile_replaces_the_active_one() -> None:
store = InMemoryGenerationProfileStore()
registry = GenerationProfileRegistry(store)
first = await registry.register(_profile())
second = await registry.register(_profile(id=uuid4(), version="v2"))
await registry.activate(first.id)
await registry.activate(second.id)
assert (await registry.active()).id == second.id
@@ -0,0 +1,335 @@
from __future__ import annotations
import argparse
import hashlib
import json
from collections.abc import Mapping
from pathlib import Path
from types import SimpleNamespace
from uuid import UUID, uuid4
import pytest
from ragcore.application.models.selection import (
EmbeddingCutoverRejected,
EmbeddingSelectionConflict,
EmbeddingSelectionNotInitialized,
EmbeddingSelectionOperationResult,
EmbeddingSelectionState,
EmbeddingSelectionVerificationResult,
)
from ragcore.config import Settings
from ragcore.domain.control import DataPolicy
from ragcore.domain.models import (
EmbeddingProfile,
EmbeddingProvider,
EmbeddingSelection,
NormalizationMode,
)
from ragcore.infrastructure.models.persistence import JsonEmbeddingProfileStore
from ragcore.ops import modelforge_embedding_bootstrap as bootstrap
_OPERATOR_TOKEN = "operator-token-with-at-least-32-random-characters"
class MemorySelectionStore:
def __init__(self) -> None:
self.state: EmbeddingSelectionState | None = None
self.operations: dict[UUID, tuple[str, str, EmbeddingSelectionOperationResult]] = {}
async def get(self) -> EmbeddingSelectionState:
if self.state is None:
raise EmbeddingSelectionNotInitialized("not initialized")
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
_action, old_fingerprint, result = entry
if old_fingerprint != request_fingerprint:
raise EmbeddingSelectionConflict("operation id conflict")
return EmbeddingSelectionOperationResult(
operation_id=result.operation_id,
generation=result.generation,
selection=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:
if operation_id in self.operations:
replay = await self.find_operation(
operation_id,
request_fingerprint=request_fingerprint,
)
assert replay is not None
return replay
if action != "initialize" or expected_generation != 0 or self.state is not None:
raise EmbeddingSelectionConflict("already initialized or stale")
self.state = EmbeddingSelectionState(0, target, None)
result = EmbeddingSelectionOperationResult(
operation_id=operation_id,
generation=0,
selection=target,
evidence=operation_evidence or {},
)
self.operations[operation_id] = (action, request_fingerprint, result)
return result
class RecordingVerifier:
def __init__(self) -> None:
self.calls: list[tuple[EmbeddingSelection, EmbeddingProfile]] = []
self.gateway_request_id = uuid4()
async def verify(
self, selection: EmbeddingSelection, profile: EmbeddingProfile
) -> EmbeddingSelectionVerificationResult:
self.calls.append((selection, profile))
return EmbeddingSelectionVerificationResult(
gateway_request_ids=(self.gateway_request_id,), verified_point_count=12
)
class ClosingResource:
async def close(self) -> None:
return None
async def aclose(self) -> None:
return None
async def dispose(self) -> None:
return None
def _rc_settings(tmp_path: Path) -> Settings:
return Settings(
env="rc",
secret_key="s" * 48,
api_token_pepper="p" * 48,
qdrant_api_key="q" * 48,
database_url="postgresql+psycopg://rc:rc-very-long-random-password@localhost:5432/ragcore_rc",
public_base_url="https://ragcore-rc.example.test",
trusted_proxy_ips=("127.0.0.1",),
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
embedding_cutover_operator_id=uuid4(),
embedding_cutover_operator_token_sha256=hashlib.sha256(
_OPERATOR_TOKEN.encode("utf-8")
).hexdigest(),
)
def _manifest(
path: Path, *, profile: str = "modelforge-rc-con01", version: str = "rc1"
) -> dict[str, object]:
value: dict[str, object] = {
"profile": profile,
"version": version,
"embedding_space_id": str(uuid4()),
"embedding_space_digest": "a" * 64,
"dimensions": 1024,
"experiment_route_key": "rc-canary-2026-09",
}
path.write_text(json.dumps(value), encoding="utf-8")
return value
def _arguments(path: Path, *, operation_id: UUID | None = None) -> argparse.Namespace:
return bootstrap.parser().parse_args(
[
"initialize",
"--expected-generation",
"0",
"--operation-id",
str(operation_id or uuid4()),
"--manifest-file",
str(path),
]
)
def test_parser_requires_generation_zero_operation_id_and_manifest() -> None:
manifest = Path("manifest.json")
args = bootstrap.parser().parse_args(
[
"initialize",
"--expected-generation",
"0",
"--operation-id",
str(uuid4()),
"--manifest-file",
str(manifest),
]
)
assert args.operation == "initialize"
assert args.expected_generation == 0
assert args.manifest_file == manifest
with pytest.raises(SystemExit):
bootstrap.parser().parse_args(
["initialize", "--expected-generation", "-1", "--operation-id", str(uuid4())]
)
def test_manifest_rejects_duplicate_or_unknown_fields(tmp_path: Path) -> None:
path = tmp_path / "manifest.json"
path.write_text('{"profile":"a","profile":"b"}', encoding="utf-8")
with pytest.raises(bootstrap.EmbeddingBootstrapOperatorError, match="valid JSON"):
bootstrap._read_manifest(path)
path.write_text(json.dumps({"unexpected": True}), encoding="utf-8")
with pytest.raises(bootstrap.EmbeddingBootstrapOperatorError, match="field set"):
bootstrap._read_manifest(path)
@pytest.mark.asyncio
async def test_bootstrap_rejects_collection_already_referenced_by_qdrant_alias() -> None:
space_id = uuid4()
profile = EmbeddingProfile(
id=space_id,
profile="modelforge-rc-con01",
version="rc1",
provider=EmbeddingProvider.MODELFORGE,
model_identifier="rag.embedding@1",
model_digest="a" * 64,
dimensions=4,
context_limit=4096,
tokenizer="capability-managed",
data_policy=DataPolicy.LOCAL_ONLY,
normalization=NormalizationMode.L2,
embedding_space_id=space_id,
)
selection = EmbeddingSelection.from_profile(profile, experiment_route_key="rc-canary-route")
class AliasedClient:
async def get_aliases(self) -> SimpleNamespace:
return SimpleNamespace(
aliases=[SimpleNamespace(collection_name=selection.collection_name)]
)
verifier = bootstrap._UnaliasedRCCollectionVerifier(AliasedClient()) # type: ignore[arg-type]
with pytest.raises(EmbeddingCutoverRejected, match="already a Qdrant alias target"):
await verifier.verify(selection, profile)
@pytest.mark.asyncio
async def test_execute_registers_and_initializes_exact_verified_rc_selection_idempotently(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = _rc_settings(tmp_path)
manifest_path = tmp_path / "bootstrap.json"
manifest = _manifest(manifest_path)
operation_id = uuid4()
profile_store = JsonEmbeddingProfileStore(settings.embedding_profiles_path)
selection_store = MemorySelectionStore()
verifier = RecordingVerifier()
from ragcore.application.models.selection import EmbeddingSelectionService
service = EmbeddingSelectionService(
profiles=profile_store,
selections=selection_store,
verifier=verifier,
)
async def build_service(
_settings: Settings,
) -> tuple[object, ClosingResource, ClosingResource, ClosingResource]:
return _service_tuple(service)
monkeypatch.setattr(bootstrap, "_build_service", build_service)
arguments = _arguments(manifest_path, operation_id=operation_id)
first = await bootstrap.execute(
arguments,
operator_token=_OPERATOR_TOKEN,
settings=settings,
)
replay = await bootstrap.execute(
arguments,
operator_token=_OPERATOR_TOKEN,
settings=settings,
)
space_id = UUID(str(manifest["embedding_space_id"]))
profile = await profile_store.get(space_id)
assert profile is not None
assert profile.model_digest == manifest["embedding_space_digest"]
assert profile.dimensions == manifest["dimensions"]
assert profile.embedding_space_id == space_id
assert await profile_store.get_active() is None
assert selection_store.state is not None
selection = selection_store.state.active
assert selection.profile_id == space_id
assert selection.experiment_route_key == manifest["experiment_route_key"]
assert selection.collection_name == "rag_dense_modelforge-rc-con01_rc1"
assert verifier.calls == [(selection, profile)]
assert first["status"] == "applied"
assert replay["status"] == "replayed"
assert first["generation"] == replay["generation"] == 0
assert first["verification"]["verified_point_count"] == 12 # type: ignore[index]
assert _OPERATOR_TOKEN not in json.dumps(first)
def _service_tuple(
service: object,
) -> tuple[object, ClosingResource, ClosingResource, ClosingResource]:
resource = ClosingResource()
return service, resource, resource, resource
@pytest.mark.asyncio
async def test_execute_rejects_non_rc_collection_namespace_before_connecting(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = _rc_settings(tmp_path)
manifest_path = tmp_path / "bootstrap.json"
_manifest(manifest_path, profile="modelforge-rag", version="m6")
async def no_connect(_settings: Settings) -> object:
raise AssertionError("service connections must follow manifest validation")
monkeypatch.setattr(bootstrap, "_build_service", no_connect)
with pytest.raises(bootstrap.EmbeddingBootstrapOperatorError, match="modelforge-rc-"):
await bootstrap.execute(
_arguments(manifest_path),
operator_token=_OPERATOR_TOKEN,
settings=settings,
)
@pytest.mark.asyncio
async def test_execute_rejects_non_rc_or_wrong_operator_token_before_manifest_read(
tmp_path: Path,
) -> None:
missing_manifest = tmp_path / "does-not-exist.json"
arguments = _arguments(missing_manifest)
with pytest.raises(bootstrap.EmbeddingBootstrapOperatorError, match="restricted"):
await bootstrap.execute(arguments, operator_token=_OPERATOR_TOKEN, settings=Settings())
settings = _rc_settings(tmp_path)
with pytest.raises(bootstrap.EmbeddingBootstrapOperatorError, match="authentication failed"):
await bootstrap.execute(
arguments,
operator_token="wrong-token-value-that-is-long-enough",
settings=settings,
)
@@ -0,0 +1,255 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from uuid import uuid4
import pytest
from pydantic import ValidationError
from ragcore.application.models.selection import EmbeddingSelectionOperationResult
from ragcore.config import Settings
from ragcore.domain.models import EmbeddingProvider, EmbeddingSelection
from ragcore.ops.modelforge_embedding_cutover import (
_PROMOTION_EVIDENCE_FIELDS,
EmbeddingCutoverOperatorError,
_authorize_operator,
_read_evidence,
_redacted_receipt,
execute,
parser,
)
_OPERATOR_TOKEN = "operator-token-with-at-least-32-random-characters"
def _rc_settings(tmp_path: Path, *, token_hash: str | None = None) -> Settings:
return Settings(
env="rc",
secret_key="s" * 48,
api_token_pepper="p" * 48,
qdrant_api_key="q" * 48,
database_url="postgresql+psycopg://rc:rc-very-long-random-password@localhost:5432/ragcore_rc",
public_base_url="https://ragcore-rc.example.test",
trusted_proxy_ips=("127.0.0.1",),
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
embedding_cutover_operator_id=uuid4() if token_hash is not None else None,
embedding_cutover_operator_token_sha256=token_hash,
)
def test_parser_requires_bounded_operation_identity_and_evidence_file(tmp_path: Path) -> None:
evidence = tmp_path / "evidence.json"
args = parser().parse_args(
[
"promote",
"--expected-generation",
"7",
"--operation-id",
str(uuid4()),
"--evidence-file",
str(evidence),
]
)
assert args.operation == "promote"
assert args.expected_generation == 7
assert args.evidence_file == evidence
with pytest.raises(SystemExit):
parser().parse_args(
["rollback", "--expected-generation", "-1", "--operation-id", str(uuid4())]
)
def test_parser_rejects_nil_operation_ids() -> None:
with pytest.raises(SystemExit):
parser().parse_args(
[
"rollback",
"--expected-generation",
"0",
"--operation-id",
"00000000-0000-0000-0000-000000000000",
"--evidence-file",
"evidence.json",
]
)
def test_operator_authentication_requires_rc_configuration_and_exact_secret(tmp_path: Path) -> None:
expected = hashlib.sha256(_OPERATOR_TOKEN.encode()).hexdigest()
settings = _rc_settings(tmp_path, token_hash=expected)
_authorize_operator(settings, _OPERATOR_TOKEN)
with pytest.raises(EmbeddingCutoverOperatorError, match="authentication failed"):
_authorize_operator(settings, "wrong-token-value-that-is-long-enough")
with pytest.raises(EmbeddingCutoverOperatorError, match="identity is not configured"):
_authorize_operator(_rc_settings(tmp_path), _OPERATOR_TOKEN)
with pytest.raises(EmbeddingCutoverOperatorError, match="restricted"):
_authorize_operator(Settings(env="development"), _OPERATOR_TOKEN)
def test_rc_settings_reject_malformed_operator_token_hash() -> None:
with pytest.raises(ValidationError, match="lowercase SHA-256"):
Settings(embedding_cutover_operator_token_sha256="not-a-hash")
def test_rc_requires_production_strength_settings() -> None:
with pytest.raises(ValidationError, match="secret_key"):
Settings(env="rc", secret_key="development-only-change-me")
def test_rc_operator_identity_and_token_hash_must_be_paired() -> None:
with pytest.raises(ValidationError, match="must be set together"):
Settings(embedding_cutover_operator_id=uuid4())
with pytest.raises(ValidationError, match="must not be the nil UUID"):
Settings(
embedding_cutover_operator_id="00000000-0000-0000-0000-000000000000",
embedding_cutover_operator_token_sha256="a" * 64,
)
def test_evidence_parser_rejects_unexpected_fields(tmp_path: Path) -> None:
path = tmp_path / "evidence.json"
path.write_text(
json.dumps({"comparison_id": str(uuid4()), "secret": "extra"}), encoding="utf-8"
)
with pytest.raises(EmbeddingCutoverOperatorError, match="unexpected field set"):
_read_evidence(path, expected_fields=_PROMOTION_EVIDENCE_FIELDS)
def test_evidence_parser_rejects_duplicate_keys(tmp_path: Path) -> None:
path = tmp_path / "evidence.json"
path.write_text('{"operator_id":"a","operator_id":"b"}', encoding="utf-8")
with pytest.raises(EmbeddingCutoverOperatorError, match="valid JSON"):
_read_evidence(path, expected_fields=frozenset({"operator_id"}))
def test_receipt_omits_submitted_evidence_and_retains_only_bounded_verification() -> None:
operation_id = uuid4()
selection = EmbeddingSelection(
profile_id=uuid4(),
profile="rc-candidate",
version="v2",
provider=EmbeddingProvider.MODELFORGE,
model_identifier="rag.embedding@1",
model_digest="a" * 64,
dimensions=4,
embedding_space_id=uuid4(),
experiment_route_key="lab-candidate-route",
collection_name="rag_dense_rc-candidate_v2",
)
result = EmbeddingSelectionOperationResult(
operation_id=operation_id,
generation=9,
selection=selection,
evidence={
"operator_id": str(uuid4()),
"approval_id": str(uuid4()),
"reason": "private change note",
"verification": {
"gateway_request_ids": [str(uuid4())],
"verified_point_count": 25,
},
},
)
receipt = _redacted_receipt("promote", result)
assert receipt["operation_id"] == str(operation_id)
assert "operator_id" not in receipt and "approval_id" not in receipt
assert "private change note" not in json.dumps(receipt)
assert receipt["verification"]["verified_point_count"] == 25 # type: ignore[index]
@pytest.mark.asyncio
async def test_execute_rejects_non_rc_before_reading_evidence_or_connecting(tmp_path: Path) -> None:
evidence = tmp_path / "missing-evidence.json"
arguments = parser().parse_args(
[
"promote",
"--expected-generation",
"0",
"--operation-id",
str(uuid4()),
"--evidence-file",
str(evidence),
]
)
with pytest.raises(EmbeddingCutoverOperatorError, match="restricted"):
await execute(arguments, settings=Settings(env="development"), operator_token="x" * 40)
@pytest.mark.asyncio
async def test_execute_rejects_evidence_claimed_by_another_operator(tmp_path: Path) -> None:
token_hash = hashlib.sha256(_OPERATOR_TOKEN.encode()).hexdigest()
settings = _rc_settings(tmp_path, token_hash=token_hash)
evidence_path = tmp_path / "promotion.json"
evidence_path.write_text(
json.dumps(
{
"comparison_id": str(uuid4()),
"comparison_reviewed": True,
"comparison_passed": True,
"operator_id": str(uuid4()),
"approval_id": str(uuid4()),
"operator_approved_impact_and_reindex": True,
"index_operation_id": str(uuid4()),
"authoritative_reindex_completed": True,
"collection_identity_pins_verified": True,
"consumer_smoke_passed": True,
"candidate_profile_id": str(uuid4()),
"candidate_experiment_route_key": "lab-candidate-route",
"gateway_route_key": "lab-candidate-route",
"gateway_embedding_space_id": str(uuid4()),
"gateway_dimensions": 4,
}
),
encoding="utf-8",
)
arguments = parser().parse_args(
[
"promote",
"--expected-generation",
"0",
"--operation-id",
str(uuid4()),
"--evidence-file",
str(evidence_path),
]
)
with pytest.raises(EmbeddingCutoverOperatorError, match="authenticated identity"):
await execute(arguments, settings=settings, operator_token=_OPERATOR_TOKEN)
@pytest.mark.asyncio
async def test_execute_rejects_invalid_evidence_before_connecting(tmp_path: Path) -> None:
token_hash = hashlib.sha256(_OPERATOR_TOKEN.encode()).hexdigest()
settings = _rc_settings(tmp_path, token_hash=token_hash)
evidence_path = tmp_path / "malformed.json"
evidence_path.write_text("not-json", encoding="utf-8")
arguments = parser().parse_args(
[
"rollback",
"--expected-generation",
"3",
"--operation-id",
str(uuid4()),
"--evidence-file",
str(evidence_path),
]
)
with pytest.raises(EmbeddingCutoverOperatorError, match="not valid JSON"):
await execute(arguments, settings=settings, operator_token=_OPERATOR_TOKEN)
@@ -0,0 +1,720 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import replace
from datetime import UTC, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from uuid import UUID, uuid4
import httpx
import pytest
from qdrant_client.http import models
from ragcore.ops.modelforge_selected_comparison import (
ComparisonCase,
ComparisonClaim,
ComparisonManifest,
ComparisonPins,
ComparisonStatus,
ComparisonTargetPin,
ModelForgeComparisonClient,
PinnedIndex,
SelectedComparisonError,
SelectedComparisonRunner,
_parse_pins,
)
from ragcore.retrieval.ranking import FusionConfig
from ragcore.retrieval.sparse import SparseEncoderProfile
COMPARISON_ID = uuid4()
PROJECT_ID = uuid4()
SUITE_REVISION_ID = uuid4()
WORKSPACE_ID = uuid4()
CASE_ID = uuid4()
CHUNK_ID = uuid4()
SPACE_ID = uuid4()
DOCUMENT_ID = uuid4()
CURRENT_DEPLOYMENT_ID = uuid4()
CANDIDATE_DEPLOYMENT_ID = uuid4()
CURRENT_SPACE_ID = uuid4()
CANDIDATE_SPACE_ID = uuid4()
def _index(ref: str, profile: str, digest: str, space_id: UUID) -> PinnedIndex:
return PinnedIndex(
ref=ref,
profile=profile,
version="v1",
dimensions=2,
model_digest=digest,
embedding_space_id=space_id,
embedding_space_digest=("e" if profile == "current" else "f") * 64,
)
def _manifest(tmp_path: Path) -> ComparisonManifest:
retrieval = {
"candidate_k": 40,
"dense_limit": 40,
"sparse_limit": 40,
"output_k": 10,
"timeout_seconds": 2.0,
"sparse_profile": {
"name": "technical",
"version": "v1",
"hash_namespace": "ragcore-sparse-v1",
"max_tokens": 4096,
"term_weight": 1.0,
"identifier_weight": 3.0,
"identifier_component_weight": 0.5,
"normalization": "none",
},
"fusion": {"rrf_k": 60, "max_fused_candidates": 40, "max_per_document": 3},
}
return ComparisonManifest(
comparison_id=COMPARISON_ID,
project_id=PROJECT_ID,
suite_revision_id=SUITE_REVISION_ID,
workspace_id=WORKSPACE_ID,
corpus_sha256="a" * 64,
consumer_revision_sha256="b" * 64,
modelforge_base_url="http://modelforge:8000",
credential_file=tmp_path / "credential",
qdrant_url="http://qdrant:6333",
current_index=_index("current-index", "current", "c" * 64, CURRENT_SPACE_ID),
candidate_index=_index("candidate-index", "candidate", "d" * 64, CANDIDATE_SPACE_ID),
retrieval_config=retrieval,
sparse_profile=SparseEncoderProfile(),
fusion_config=FusionConfig(),
output_k=10,
timeout_seconds=2.0,
)
def _write_credential(path: Path) -> None:
path.write_text("mf_" + "x" * 48, encoding="utf-8")
path.chmod(0o600)
def _manifest_payload(manifest: ComparisonManifest) -> dict[str, object]:
def index_payload(index: PinnedIndex) -> dict[str, object]:
return {
"ref": index.ref,
"profile": index.profile,
"version": index.version,
"dimensions": index.dimensions,
"model_digest": index.model_digest,
"embedding_space_id": str(index.embedding_space_id),
"embedding_space_digest": index.embedding_space_digest,
}
return {
"comparison_id": str(manifest.comparison_id),
"project_id": str(manifest.project_id),
"suite_revision_id": str(manifest.suite_revision_id),
"workspace_id": str(manifest.workspace_id),
"corpus_sha256": manifest.corpus_sha256,
"consumer_revision_sha256": manifest.consumer_revision_sha256,
"modelforge_base_url": manifest.modelforge_base_url,
"credential_file": str(manifest.credential_file),
"qdrant_url": manifest.qdrant_url,
"current_index": index_payload(manifest.current_index),
"candidate_index": index_payload(manifest.candidate_index),
"retrieval_config": dict(manifest.retrieval_config),
}
def _claim(
manifest: ComparisonManifest,
*,
current_space: UUID = CURRENT_SPACE_ID,
current_route_key: str | None = None,
) -> ComparisonClaim:
now = datetime.now(UTC)
current = ComparisonTargetPin(
deployment_id=CURRENT_DEPLOYMENT_ID,
embedding_space_id=current_space,
embedding_space_digest="e" * 64,
dimension=2,
artifact_sha256="1" * 64,
runtime_fingerprint="runtime-current",
)
candidate = ComparisonTargetPin(
deployment_id=CANDIDATE_DEPLOYMENT_ID,
embedding_space_id=CANDIDATE_SPACE_ID,
embedding_space_digest="f" * 64,
dimension=2,
artifact_sha256="2" * 64,
runtime_fingerprint="runtime-candidate",
)
raw_pins = {
"project_id": str(PROJECT_ID),
"current_index_ref": manifest.current_index.ref,
"candidate_index_ref": manifest.candidate_index.ref,
"corpus_sha256": manifest.corpus_sha256,
"retrieval_config_sha256": manifest.retrieval_config_sha256,
"candidate_route_key": "candidate-route",
"current": {
"deployment_id": str(CURRENT_DEPLOYMENT_ID),
"embedding_space_id": str(current_space),
"embedding_space_digest": "e" * 64,
"dimension": 2,
"artifact_sha256": "1" * 64,
"runtime_fingerprint": "runtime-current",
},
"candidate": {
"deployment_id": str(CANDIDATE_DEPLOYMENT_ID),
"embedding_space_id": str(CANDIDATE_SPACE_ID),
"embedding_space_digest": "f" * 64,
"dimension": 2,
"artifact_sha256": "2" * 64,
"runtime_fingerprint": "runtime-candidate",
},
}
if current_route_key is not None:
raw_pins["current_route_key"] = current_route_key
return ComparisonClaim(
comparison_id=COMPARISON_ID,
suite_revision_id=SUITE_REVISION_ID,
current_deployment_id=CURRENT_DEPLOYMENT_ID,
candidate_deployment_id=CANDIDATE_DEPLOYMENT_ID,
expected_cases=1,
claim_token="claim-token-" + "x" * 32,
claim_expires_at=now + timedelta(seconds=120),
deadline_at=now + timedelta(minutes=10),
pins_digest=hashlib.sha256(
json.dumps(raw_pins, sort_keys=True, separators=(",", ":"), default=str).encode()
).hexdigest(),
pins=ComparisonPins(
raw=raw_pins,
project_id=PROJECT_ID,
corpus_sha256=manifest.corpus_sha256,
retrieval_config_sha256=manifest.retrieval_config_sha256,
current_index_ref=manifest.current_index.ref,
candidate_index_ref=manifest.candidate_index.ref,
current_route_key=current_route_key,
candidate_route_key="candidate-route",
current=current,
candidate=candidate,
),
cases=(
ComparisonCase(
id=CASE_ID,
suite_revision_id=SUITE_REVISION_ID,
case_key="one",
query="where is the answer",
relevant_chunk_ids=(CHUNK_ID,),
critical=True,
),
),
)
class FakeControl:
def __init__(self, claim: ComparisonClaim) -> None:
self.claim_value = claim
self.progress_calls: list[tuple[int, int]] = []
self.finish_calls: list[tuple[str, str]] = []
self.submitted: tuple[object, ...] | None = None
self.cancel = False
async def claim(self, comparison_id: UUID) -> ComparisonClaim:
assert comparison_id == self.claim_value.comparison_id
return self.claim_value
async def progress(
self,
comparison_id: UUID,
claim_token: str,
current_completed: int,
candidate_completed: int,
) -> ComparisonStatus:
assert comparison_id == self.claim_value.comparison_id
assert claim_token == self.claim_value.claim_token
self.progress_calls.append((current_completed, candidate_completed))
return ComparisonStatus(
status="cancel_requested" if self.cancel else "running",
current_completed_cases=current_completed,
candidate_completed_cases=candidate_completed,
)
async def submit_result(
self,
comparison_id: UUID,
claim_token: str,
pins_digest: str,
consumer_revision_sha256: str,
current_results: object,
candidate_results: object,
current_gateway_requests: object,
candidate_gateway_requests: object,
) -> ComparisonStatus:
self.submitted = (
pins_digest,
consumer_revision_sha256,
current_results,
candidate_results,
current_gateway_requests,
candidate_gateway_requests,
)
return ComparisonStatus(
status="completed",
current_completed_cases=1,
candidate_completed_cases=1,
baseline_run_id=uuid4(),
candidate_run_id=uuid4(),
evaluation_comparison_id=uuid4(),
)
async def finish(
self, comparison_id: UUID, claim_token: str, outcome: str, error_code: str
) -> ComparisonStatus:
self.finish_calls.append((outcome, error_code))
return ComparisonStatus(
status=outcome,
current_completed_cases=0,
candidate_completed_cases=0,
error_code=error_code,
)
class FakeQdrant:
def __init__(self, manifest: ComparisonManifest) -> None:
self.manifest = manifest
self.request_ids: list[str] = []
async def retrieve(self, *, collection_name: str, ids: list[str], **_: object) -> list[object]:
assert collection_name == self.manifest.current_index.ref
assert ids == [str(CHUNK_ID)]
return [
SimpleNamespace(
id=str(CHUNK_ID),
payload={"workspace_id": str(WORKSPACE_ID), "space_id": str(SPACE_ID)},
)
]
async def get_aliases(self) -> SimpleNamespace:
return SimpleNamespace(
aliases=[
SimpleNamespace(
alias_name="rag_dense_current_active",
collection_name=self.manifest.current_index.ref,
)
]
)
async def get_collection(self, collection_name: str) -> SimpleNamespace:
assert collection_name in {
self.manifest.current_index.ref,
self.manifest.candidate_index.ref,
}
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, *, collection_name: str, **_: object) -> models.QueryResponse:
index = (
self.manifest.current_index
if collection_name == self.manifest.current_index.ref
else self.manifest.candidate_index
)
point_id = CHUNK_ID
self.request_ids.append(collection_name)
return models.QueryResponse(
points=[
models.ScoredPoint(
id=str(point_id),
version=0,
score=0.9,
payload={
"workspace_id": str(WORKSPACE_ID),
"space_id": str(SPACE_ID),
"document_id": str(DOCUMENT_ID),
"document_version_id": str(UUID("10000000-0000-0000-0000-000000000001")),
"chunk_id": str(CHUNK_ID),
"source_id": str(UUID("10000000-0000-0000-0000-000000000002")),
"parent_chunk_id": None,
"status": "published",
"content_type": "text/plain",
"language": "en",
"trust_level": "source",
"embedding_model_digest": index.model_digest,
"chunk_profile_version": index.version,
"embedding_space_id": str(index.embedding_space_id),
},
)
]
)
def _gateway_transport(
manifest: ComparisonManifest,
*,
duplicate: bool = False,
current_route_key: str | None = None,
fail_route_key: str | None = None,
seen: list[str] | None = None,
) -> httpx.MockTransport:
request_id = uuid4()
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path.endswith("/invoke"):
assert json.loads(request.content)["input"] == ["where is the answer"]
if seen is not None:
seen.append(request.url.path)
if fail_route_key is not None and request.url.path.endswith(
f"/{fail_route_key}/invoke"
):
return httpx.Response(503)
if current_route_key is not None and request.url.path.endswith(
f"/{current_route_key}/invoke"
):
space_id = manifest.current_index.embedding_space_id
response_id = uuid4()
elif request.url.path.endswith("/candidate-route/invoke"):
space_id = manifest.candidate_index.embedding_space_id
response_id = request_id if duplicate else uuid4()
else:
space_id = manifest.current_index.embedding_space_id
response_id = request_id
return httpx.Response(
200,
json={
"capability": "rag.embedding@1",
"dimension": 2,
"normalized": True,
"embedding_space_id": str(space_id),
"data": [[1.0, 0.0]],
"request_id": str(response_id),
},
)
raise AssertionError(request.url.path)
return httpx.MockTransport(handler)
@pytest.mark.asyncio
async def test_selected_runner_executes_both_pinned_legs_and_submits_gateway_evidence(
tmp_path: Path,
) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
control = FakeControl(_claim(manifest))
qdrant = FakeQdrant(manifest)
seen: list[str] = []
async with httpx.AsyncClient(
transport=_gateway_transport(manifest, seen=seen), follow_redirects=False
) as client:
result = await SelectedComparisonRunner(manifest, control, qdrant, client).run()
assert result["status"] == "completed"
assert control.progress_calls == [(1, 0), (1, 1)]
assert control.finish_calls == []
assert control.submitted is not None
assert qdrant.request_ids == [
"current-index",
"current-index",
"candidate-index",
"candidate-index",
]
assert result["current_gateway_requests"] != result["candidate_gateway_requests"]
assert seen == [
"/api/v1/capabilities/rag.embedding@1/invoke",
"/api/v1/capability-experiments/candidate-route/invoke",
]
@pytest.mark.asyncio
async def test_selected_runner_uses_optional_current_lab_route_pin(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
current_route_key = "current-lab-route"
control = FakeControl(_claim(manifest, current_route_key=current_route_key))
seen: list[str] = []
async with httpx.AsyncClient(
transport=_gateway_transport(manifest, current_route_key=current_route_key, seen=seen),
follow_redirects=False,
) as client:
result = await SelectedComparisonRunner(
manifest, control, FakeQdrant(manifest), client
).run()
assert result["status"] == "completed"
assert seen == [
"/api/v1/capability-experiments/current-lab-route/invoke",
"/api/v1/capability-experiments/candidate-route/invoke",
]
def test_claim_pins_reject_malformed_optional_current_route_key(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
malformed = {**_claim(manifest).pins.raw, "current_route_key": "../stable"}
with pytest.raises(SelectedComparisonError, match="CURRENT_ROUTE_KEY_INVALID"):
_parse_pins(malformed)
@pytest.mark.asyncio
async def test_current_lab_route_failure_does_not_fall_back_to_stable_or_ollama(
tmp_path: Path,
) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
current_route_key = "current-lab-route"
control = FakeControl(_claim(manifest, current_route_key=current_route_key))
seen: list[str] = []
async with httpx.AsyncClient(
transport=_gateway_transport(
manifest,
current_route_key=current_route_key,
fail_route_key=current_route_key,
seen=seen,
),
follow_redirects=False,
) as client:
result = await SelectedComparisonRunner(
manifest, control, FakeQdrant(manifest), client
).run()
assert result["status"] == "failed"
assert seen == ["/api/v1/capability-experiments/current-lab-route/invoke"]
assert control.submitted is None
@pytest.mark.asyncio
async def test_selected_runner_fails_closed_on_duplicate_gateway_identity(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
control = FakeControl(_claim(manifest))
async with httpx.AsyncClient(
transport=_gateway_transport(manifest, duplicate=True), follow_redirects=False
) as client:
result = await SelectedComparisonRunner(
manifest, control, FakeQdrant(manifest), client
).run()
assert result["status"] == "failed"
assert result["error_code"] == "DUPLICATE_GATEWAY_REQUEST_ID"
assert control.submitted is None
assert control.finish_calls == [("failed", "DUPLICATE_GATEWAY_REQUEST_ID")]
@pytest.mark.asyncio
async def test_selected_runner_honours_cancellation_before_candidate_leg(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
control = FakeControl(_claim(manifest))
control.cancel = True
qdrant = FakeQdrant(manifest)
async with httpx.AsyncClient(
transport=_gateway_transport(manifest), follow_redirects=False
) as client:
result = await SelectedComparisonRunner(manifest, control, qdrant, client).run()
assert result["status"] == "cancelled"
assert result["error_code"] == "COMPARISON_CANCEL_REQUESTED"
assert result["completed_cases"] == 0
assert control.submitted is None
assert control.finish_calls == [("cancelled", "COMPARISON_CANCEL_REQUESTED")]
assert qdrant.request_ids == ["current-index", "current-index"]
class FailingProgressControl(FakeControl):
async def progress(
self,
comparison_id: UUID,
claim_token: str,
current_completed: int,
candidate_completed: int,
) -> ComparisonStatus:
del comparison_id, claim_token, current_completed, candidate_completed
raise SelectedComparisonError("LEASE_RENEWAL_FAILED")
@pytest.mark.asyncio
async def test_selected_runner_reports_lease_failure_without_submitting_partial_results(
tmp_path: Path,
) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
control = FailingProgressControl(_claim(manifest))
async with httpx.AsyncClient(
transport=_gateway_transport(manifest), follow_redirects=False
) as client:
result = await SelectedComparisonRunner(
manifest, control, FakeQdrant(manifest), client
).run()
assert result["status"] == "failed"
assert result["error_code"] == "LEASE_RENEWAL_FAILED"
assert result["completed_cases"] == 0
assert control.submitted is None
assert control.finish_calls == [("failed", "LEASE_RENEWAL_FAILED")]
@pytest.mark.asyncio
async def test_selected_runner_rejects_expired_claim_before_inference(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
claim = replace(
_claim(manifest),
claim_expires_at=datetime.now(UTC) - timedelta(seconds=1),
deadline_at=datetime.now(UTC) + timedelta(minutes=5),
)
control = FakeControl(claim)
async with httpx.AsyncClient(transport=_gateway_transport(manifest)) as client:
result = await SelectedComparisonRunner(
manifest, control, FakeQdrant(manifest), client
).run()
assert result["status"] == "failed"
assert result["error_code"] == "COMPARISON_CLAIM_EXPIRED"
assert control.finish_calls == [("failed", "COMPARISON_CLAIM_EXPIRED")]
def test_manifest_requires_exact_runtime_contract(tmp_path: Path) -> None:
manifest = {
"comparison_id": str(COMPARISON_ID),
"project_id": str(PROJECT_ID),
"suite_revision_id": str(SUITE_REVISION_ID),
"workspace_id": str(WORKSPACE_ID),
"corpus_sha256": "a" * 64,
"consumer_revision_sha256": "b" * 64,
"modelforge_base_url": "https://api.example.com",
"credential_file": "credential",
"qdrant_url": "http://qdrant:6333",
"current_index": {},
"candidate_index": {},
"retrieval_config": {},
}
path = tmp_path / "manifest.json"
path.write_text(json.dumps(manifest), encoding="utf-8")
with pytest.raises(SelectedComparisonError, match="MODELFORGE_BASE_URL_MUST_BE_LOCAL_ORIGIN"):
ComparisonManifest.from_path(path)
def test_manifest_loads_the_pinned_index_and_retrieval_contract(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
path = tmp_path / "manifest.json"
path.write_text(json.dumps(_manifest_payload(manifest)), encoding="utf-8")
loaded = ComparisonManifest.from_path(path)
assert loaded.current_index == manifest.current_index
assert loaded.candidate_index == manifest.candidate_index
assert loaded.retrieval_config_sha256 == manifest.retrieval_config_sha256
assert loaded.sparse_profile.digest == manifest.sparse_profile.digest
@pytest.mark.asyncio
async def test_control_client_parses_and_verifies_claim_pins(tmp_path: Path) -> None:
manifest = _manifest(tmp_path)
_write_credential(manifest.credential_file)
claim = _claim(manifest, current_route_key="current-lab-route")
response_body = {
"comparison": {
"id": str(claim.comparison_id),
"suite_revision_id": str(claim.suite_revision_id),
"current_deployment_id": str(claim.current_deployment_id),
"candidate_deployment_id": str(claim.candidate_deployment_id),
"expected_cases": claim.expected_cases,
"deadline_at": claim.deadline_at.isoformat(),
"pins": claim.pins.raw,
"pins_digest": claim.pins_digest,
},
"claim_token": claim.claim_token,
"claim_expires_at": claim.claim_expires_at.isoformat(),
"cases": [
{
"id": str(CASE_ID),
"suite_revision_id": str(SUITE_REVISION_ID),
"case_key": "one",
"query": "where is the answer",
"relevant_chunk_ids": [str(CHUNK_ID)],
"relevant_document_ids": [],
"relevance_grades": {},
"label_provenance": {},
"critical": True,
"review_status": "reviewed",
"created_at": claim.claim_expires_at.isoformat(),
}
],
}
def handler(_: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=response_body)
async with httpx.AsyncClient(
transport=httpx.MockTransport(handler), follow_redirects=False
) as client:
parsed = await ModelForgeComparisonClient(
client,
base_url=manifest.modelforge_base_url,
credential_file=manifest.credential_file,
).claim(COMPARISON_ID)
assert parsed.pins.current.dimension == 2
assert parsed.pins.current_route_key == "current-lab-route"
assert parsed.cases[0].relevant_chunk_ids == (CHUNK_ID,)
@pytest.mark.asyncio
async def test_control_client_serializes_uuid_results_without_secret_echo(tmp_path: Path) -> None:
credential = tmp_path / "credential"
_write_credential(credential)
seen: list[httpx.Request] = []
response_body = {
"status": "completed",
"current_completed_cases": 1,
"candidate_completed_cases": 1,
"claim_expires_at": None,
"deadline_at": None,
"error_code": None,
"baseline_run_id": None,
"candidate_run_id": None,
"evaluation_comparison_id": None,
}
def handler(request: httpx.Request) -> httpx.Response:
seen.append(request)
assert "mf_" not in request.url.path
return httpx.Response(200, json=response_body)
case_id, result_id = uuid4(), uuid4()
async with httpx.AsyncClient(
transport=httpx.MockTransport(handler), follow_redirects=False
) as client:
api = ModelForgeComparisonClient(
client,
base_url="http://modelforge:8000",
credential_file=credential,
)
await api.submit_result(
COMPARISON_ID,
"claim-token-" + "x" * 32,
"a" * 64,
"b" * 64,
[
{
"case_id": case_id,
"ranked_results": [{"chunk_id": result_id, "score": 0.5}],
"latency_ms": 1.0,
"error_code": None,
}
],
[{"case_id": case_id, "ranked_results": [], "latency_ms": 1.0, "error_code": None}],
{case_id: uuid4()},
{case_id: uuid4()},
)
body = seen[0].content.decode("utf-8")
assert str(result_id) in body
assert "claim-token" in body
assert "mf_" not in body
+105
View File
@@ -0,0 +1,105 @@
"""Unit coverage for canonical text/code parsing and pinned Docling selection."""
from __future__ import annotations
from uuid import uuid4
import pytest
from ragcore.domain.sources import SourceFingerprint
from ragcore.infrastructure.parsers import (
DoclingConversion,
DoclingParser,
PinnedDoclingError,
TextParser,
)
from ragcore.ingestion.parsing import (
CanonicalBlockKind,
ParseRequest,
ParserRouter,
UnsupportedParserError,
)
def parse_request(content: bytes, name: str, media_type: str) -> ParseRequest:
return ParseRequest(uuid4(), SourceFingerprint("a" * 64), content, media_type, name)
@pytest.mark.asyncio
async def test_markdown_preserves_headings_tables_code_and_locations() -> None:
parser = TextParser()
request = parse_request(
b"# Title\n\nIntro\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\n```python\nprint(1)\n```\n",
"runbook.md",
"text/markdown",
)
artifact = await parser.parse(request)
assert [block.kind for block in artifact.blocks] == [
CanonicalBlockKind.HEADING,
CanonicalBlockKind.PARAGRAPH,
CanonicalBlockKind.TABLE,
CanonicalBlockKind.CODE,
]
assert artifact.blocks[0].source_location == {"line_start": 1, "line_end": 1}
assert artifact.blocks[2].content.startswith("| A | B |")
assert artifact.blocks[3].metadata["language"] == "python"
assert artifact.blocks[3].source_location == {"line_start": 9, "line_end": 11}
@pytest.mark.asyncio
async def test_router_uses_code_parser_before_text_fallback() -> None:
from ragcore.infrastructure.parsers import CodeParser
router = ParserRouter((CodeParser(), TextParser()))
artifact = await router.parse(
parse_request(b"def f():\n return 1\n", "main.py", "text/plain")
)
assert artifact.extraction_report.normalizer_profile == "code-v1"
assert artifact.blocks[0].kind is CanonicalBlockKind.CODE
with pytest.raises(UnsupportedParserError):
await ParserRouter((TextParser(),)).parse(
parse_request(b"%PDF-1.7", "file.pdf", "application/pdf")
)
class Backend:
version = "2.117.0"
def __init__(self) -> None:
self.ocr_values: list[bool] = []
async def convert(self, content: bytes, *, enable_ocr: bool) -> DoclingConversion:
self.ocr_values.append(enable_ocr)
return DoclingConversion(())
@pytest.mark.asyncio
async def test_docling_ocr_is_selected_only_without_a_text_layer() -> None:
backend = Backend()
parser = DoclingParser(backend)
await parser.parse(parse_request(b"pdf", "scan.pdf", "application/pdf"))
request = parse_request(b"pdf", "scan.pdf", "application/pdf")
request = ParseRequest(
request.document_version_id,
request.source_fingerprint,
request.content,
request.media_type,
request.original_name,
ocr_required=True,
text_layer_present=False,
)
artifact = await parser.parse(request)
assert backend.ocr_values == [False, True]
assert artifact.extraction_report.ocr_used is True
def test_docling_minor_version_is_pinned() -> None:
class OldBackend(Backend):
version = "2.47.9"
with pytest.raises(PinnedDoclingError):
DoclingParser(OldBackend())
@@ -0,0 +1,40 @@
"""safe_attributes: sensitive query/document text is absent by default."""
from __future__ import annotations
from ragcore.platform.telemetry import safe_attributes
def test_sensitive_keys_are_dropped_entirely() -> None:
result = safe_attributes(
{
"query": "what is the secret plan",
"document": "confidential contents",
"prompt": "system prompt text",
"answer": "the generated answer",
"excerpt": "a citation excerpt",
"duration_ms": 12.5,
}
)
assert result == {"duration_ms": 12.5}
def test_matching_is_case_insensitive_and_substring_based() -> None:
result = safe_attributes(
{"UserQuery": "text", "Document_Id_Content": "text", "status_code": 200}
)
assert result == {"status_code": 200}
def test_unsupported_value_types_are_dropped() -> None:
result = safe_attributes({"safe_number": 1, "unsafe_dict": {"a": 1}, "unsafe_list": [1, 2]})
assert result == {"safe_number": 1}
def test_boolean_and_string_values_pass_through() -> None:
result = safe_attributes({"degraded": True, "operation": "search"})
assert result == {"degraded": True, "operation": "search"}
def test_empty_attributes_produce_an_empty_result() -> None:
assert safe_attributes({}) == {}
@@ -0,0 +1,65 @@
"""ops/dashboards and ops/alerts stay in sync with the real metric/attribute names."""
from __future__ import annotations
from pathlib import Path
import yaml
from ragcore.platform.telemetry import TelemetryMetrics
_REPO_ROOT = Path(__file__).parents[4]
def _known_metric_names() -> set[str]:
"""The stable metric names TelemetryMetrics actually creates."""
return {
"ragcore_request_duration_seconds",
"ragcore_request_errors_total",
"ragcore_job_queue_depth",
"ragcore_job_age_seconds",
"ragcore_qdrant_errors_total",
"ragcore_provider_errors_total",
}
def test_operations_dashboard_only_references_real_metric_names() -> None:
document = yaml.safe_load(
(_REPO_ROOT / "ops" / "dashboards" / "operations_overview.yaml").read_text()
)
known = _known_metric_names()
for panel in document["panels"]:
assert panel["metric"] in known, f"{panel['metric']} is not a real TelemetryMetrics metric"
def test_operations_dashboard_correlation_attribute_matches_the_real_span_attribute() -> None:
document = yaml.safe_load(
(_REPO_ROOT / "ops" / "dashboards" / "operations_overview.yaml").read_text()
)
assert document["correlation"]["attribute"] == "correlation_id"
def test_queue_age_alert_references_a_real_metric_name() -> None:
document = yaml.safe_load((_REPO_ROOT / "ops" / "alerts" / "queue_age_slo.yaml").read_text())
assert document["alert"] == "JobQueueAgeSloBreached"
assert document["signal"]["metric"] in _known_metric_names()
assert document["signal"]["metric"] == "ragcore_job_age_seconds"
def test_declared_metric_names_are_exhaustive_against_the_real_instruments() -> None:
"""Guards the other direction: if a new metric is added to
TelemetryMetrics without updating the dashboard, this fails instead of
the dashboard silently going stale."""
telemetry = TelemetryMetrics(meter_name="dashboard-completeness-check")
instrument_attrs = (
"request_duration_seconds",
"request_errors_total",
"job_queue_depth",
"job_age_seconds",
"qdrant_errors_total",
"provider_errors_total",
)
for attr in instrument_attrs:
assert hasattr(telemetry, attr)
@@ -0,0 +1,164 @@
"""TelemetryMetrics: the representative required-metrics subset, recorded correctly."""
from __future__ import annotations
from typing import Any
import pytest
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.resources import Resource
from ragcore.platform.telemetry import TelemetryMetrics
def _telemetry() -> tuple[TelemetryMetrics, InMemoryMetricReader]:
"""A fresh, isolated MeterProvider per call.
OpenTelemetry's global provider may only be set once per process, so
each test builds its own explicit provider and passes it directly to
``TelemetryMetrics`` rather than fighting over global state.
"""
reader = InMemoryMetricReader()
provider = MeterProvider(
resource=Resource.create({"service.name": "test"}), metric_readers=[reader]
)
return TelemetryMetrics(meter_provider=provider), reader
def _metric_named(reader: InMemoryMetricReader, name: str) -> Any | None:
data = reader.get_metrics_data()
if data is None:
return None
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 metric
return None
def _data_points(metric: Any) -> list[Any]:
return list(metric.data.data_points)
def test_successful_requests_record_duration_but_no_error() -> None:
telemetry, reader = _telemetry()
telemetry.request_duration_seconds.record(0.042, attributes={"status_code": 200})
duration_metric = _metric_named(reader, "ragcore_request_duration_seconds")
assert duration_metric is not None
points = _data_points(duration_metric)
assert len(points) == 1
assert points[0].sum == pytest.approx(0.042)
def test_server_errors_and_rate_limits_increment_the_error_counter() -> None:
telemetry, reader = _telemetry()
telemetry.record_request(duration_seconds=0.1, status_code=200)
telemetry.record_request(duration_seconds=0.2, status_code=503)
telemetry.record_request(duration_seconds=0.3, status_code=429)
error_metric = _metric_named(reader, "ragcore_request_errors_total")
assert error_metric is not None
total = sum(point.value for point in _data_points(error_metric))
assert total == 2
def test_job_queue_depth_rises_on_enqueue_and_falls_on_claim() -> None:
telemetry, reader = _telemetry()
telemetry.record_job_enqueued()
telemetry.record_job_enqueued()
telemetry.record_job_claimed(age_seconds=5.0)
depth_metric = _metric_named(reader, "ragcore_job_queue_depth")
assert depth_metric is not None
net_depth = sum(point.value for point in _data_points(depth_metric))
assert net_depth == 1
age_metric = _metric_named(reader, "ragcore_job_age_seconds")
assert age_metric is not None
assert _data_points(age_metric)[0].sum == pytest.approx(5.0)
def test_qdrant_and_provider_errors_are_recorded_by_operation_and_provider() -> None:
telemetry, reader = _telemetry()
telemetry.record_qdrant_error(operation="upsert")
telemetry.record_provider_error(provider="ollama-generation")
qdrant_metric = _metric_named(reader, "ragcore_qdrant_errors_total")
provider_metric = _metric_named(reader, "ragcore_provider_errors_total")
assert qdrant_metric is not None
assert provider_metric is not None
(qdrant_point,) = _data_points(qdrant_metric)
(provider_point,) = _data_points(provider_metric)
assert dict(qdrant_point.attributes)["operation"] == "upsert"
assert dict(provider_point.attributes)["provider"] == "ollama-generation"
def test_database_errors_are_recorded_by_operation() -> None:
telemetry, reader = _telemetry()
telemetry.record_database_error(operation="publish_validated")
metric = _metric_named(reader, "ragcore_database_errors_total")
assert metric is not None
(point,) = _data_points(metric)
assert dict(point.attributes)["operation"] == "publish_validated"
def test_provider_duration_is_recorded_by_provider() -> None:
telemetry, reader = _telemetry()
telemetry.record_provider_duration(provider="ollama-embedding", duration_seconds=0.25)
metric = _metric_named(reader, "ragcore_provider_duration_seconds")
assert metric is not None
(point,) = _data_points(metric)
assert dict(point.attributes)["provider"] == "ollama-embedding"
assert point.sum == pytest.approx(0.25)
def test_retrieval_duration_and_zero_result_are_recorded() -> None:
telemetry, reader = _telemetry()
telemetry.record_retrieval(duration_seconds=0.05, zero_result=False)
telemetry.record_retrieval(duration_seconds=0.03, zero_result=True)
duration_metric = _metric_named(reader, "ragcore_retrieval_duration_seconds")
zero_result_metric = _metric_named(reader, "ragcore_retrieval_zero_result_total")
assert duration_metric is not None
assert zero_result_metric is not None
points = _data_points(duration_metric)
assert len(points) == 1
assert points[0].count == 2
assert sum(point.value for point in _data_points(zero_result_metric)) == 1
def test_answerability_result_is_recorded() -> None:
telemetry, reader = _telemetry()
telemetry.record_answerability(result="answerable")
telemetry.record_answerability(result="unanswerable")
metric = _metric_named(reader, "ragcore_answerability_result_total")
assert metric is not None
results = {dict(point.attributes)["result"] for point in _data_points(metric)}
assert results == {"answerable", "unanswerable"}
def test_ingestion_job_result_is_recorded_by_operation_and_result() -> None:
telemetry, reader = _telemetry()
telemetry.record_ingestion_job_result(operation="ingest_upload", result="succeeded")
metric = _metric_named(reader, "ragcore_ingestion_job_result_total")
assert metric is not None
(point,) = _data_points(metric)
assert dict(point.attributes) == {"operation": "ingest_upload", "result": "succeeded"}
def test_rate_limit_refusal_is_recorded_by_operation() -> None:
telemetry, reader = _telemetry()
telemetry.record_rate_limit_refusal(operation="search")
metric = _metric_named(reader, "ragcore_rate_limit_refusals_total")
assert metric is not None
(point,) = _data_points(metric)
assert dict(point.attributes)["operation"] == "search"
@@ -0,0 +1,129 @@
"""correlated_span: one real OpenTelemetry trace across separate components."""
from __future__ import annotations
from collections.abc import Iterator
import pytest
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from ragcore.platform.telemetry import correlated_span, trace_id_from_correlation_id
@pytest.fixture
def tracing() -> Iterator[tuple[TracerProvider, InMemorySpanExporter]]:
"""A fresh, isolated TracerProvider per test.
OpenTelemetry's global provider may only be set once per process, so
each test gets its own explicit provider passed to ``correlated_span``
rather than fighting over global state.
"""
span_exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(span_exporter))
yield provider, span_exporter
def test_trace_id_is_deterministic_for_the_same_correlation_id() -> None:
assert trace_id_from_correlation_id("req-123") == trace_id_from_correlation_id("req-123")
def test_trace_id_differs_for_different_correlation_ids() -> None:
assert trace_id_from_correlation_id("req-123") != trace_id_from_correlation_id("req-456")
def test_trace_id_is_never_zero() -> None:
assert trace_id_from_correlation_id("x") != 0
def test_empty_correlation_id_is_rejected() -> None:
with pytest.raises(ValueError, match="correlation_id"):
trace_id_from_correlation_id("")
def test_two_spans_from_different_components_share_one_trace(
tracing: tuple[TracerProvider, InMemorySpanExporter],
) -> None:
"""The core acceptance proof: an "API request" span and a later "job
processing" span, opened independently with the same correlation id,
land in the literal same OpenTelemetry trace -- not just a matching
attribute a human has to notice.
"""
provider, exporter = tracing
correlation_id = "req-shared-across-request-and-job"
with correlated_span(
"api.handle_request", correlation_id=correlation_id, tracer_provider=provider
):
pass
with correlated_span("job.process", correlation_id=correlation_id, tracer_provider=provider):
pass
spans = exporter.get_finished_spans()
assert len(spans) == 2
trace_ids = {span.context.trace_id for span in spans}
assert len(trace_ids) == 1
assert trace_ids.pop() == trace_id_from_correlation_id(correlation_id)
def test_spans_from_different_correlation_ids_are_in_different_traces(
tracing: tuple[TracerProvider, InMemorySpanExporter],
) -> None:
provider, exporter = tracing
with correlated_span("api.handle_request", correlation_id="req-1", tracer_provider=provider):
pass
with correlated_span("api.handle_request", correlation_id="req-2", tracer_provider=provider):
pass
spans = exporter.get_finished_spans()
trace_ids = {span.context.trace_id for span in spans}
assert len(trace_ids) == 2
def test_correlation_id_is_always_set_as_a_safe_attribute(
tracing: tuple[TracerProvider, InMemorySpanExporter],
) -> None:
provider, exporter = tracing
with correlated_span("api.handle_request", correlation_id="req-999", tracer_provider=provider):
pass
(span,) = exporter.get_finished_spans()
assert span.attributes["correlation_id"] == "req-999"
def test_sensitive_attributes_are_dropped_before_reaching_the_span(
tracing: tuple[TracerProvider, InMemorySpanExporter],
) -> None:
provider, exporter = tracing
with correlated_span(
"api.handle_request",
correlation_id="req-1",
tracer_provider=provider,
attributes={"query": "sensitive text", "duration_ms": 42},
):
pass
(span,) = exporter.get_finished_spans()
assert "query" not in span.attributes
assert span.attributes["duration_ms"] == 42
def test_an_exception_inside_the_span_is_recorded_and_reraised(
tracing: tuple[TracerProvider, InMemorySpanExporter],
) -> None:
provider, exporter = tracing
with (
pytest.raises(RuntimeError, match="boom"),
correlated_span("api.handle_request", correlation_id="req-1", tracer_provider=provider),
):
raise RuntimeError("boom")
(span,) = exporter.get_finished_spans()
assert span.status.status_code == trace.StatusCode.ERROR
assert any(event.name == "exception" for event in span.events)
@@ -0,0 +1,216 @@
"""AnswerabilityClassifier: deterministic, content-free, non-probabilistic decisions."""
from __future__ import annotations
from uuid import UUID, uuid4
import pytest
from ragcore.retrieval.answerability import (
MAX_EVIDENCE_ITEMS,
AnswerabilityClassifier,
AnswerabilityDecision,
AnswerabilityReason,
AnswerabilityStatus,
AnswerabilityThresholds,
EvidenceSignal,
)
def make_signal(**changes: object) -> EvidenceSignal:
value: dict[str, object] = {
"document_id": uuid4(),
"source_id": uuid4(),
"trust_level": "source",
"fused_score": 0.05,
"rerank_score": 0.8,
"covers_distinct_facet": True,
"is_fresh": True,
"exact_identifier_match": False,
}
value.update(changes)
return EvidenceSignal(**value)
def default_thresholds(**changes: object) -> AnswerabilityThresholds:
value: dict[str, object] = {"version": "answerability-v1"}
value.update(changes)
return AnswerabilityThresholds(**value)
def test_sufficient_diverse_evidence_is_answerable() -> None:
evidence = (make_signal(), make_signal())
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.ANSWERABLE
assert decision.reasons == (AnswerabilityReason.SUFFICIENT_EVIDENCE,)
assert decision.evidence_count == 2
assert decision.document_diversity == 2
assert decision.source_independence == 2
assert decision.facet_coverage_ratio == 1.0
assert decision.thresholds_version == "answerability-v1"
def test_empty_evidence_is_not_answerable() -> None:
decision = AnswerabilityClassifier(default_thresholds()).classify(())
assert decision.status is AnswerabilityStatus.NOT_ANSWERABLE
assert AnswerabilityReason.INSUFFICIENT_EVIDENCE in decision.reasons
assert decision.evidence_count == 0
assert decision.document_diversity == 0
def test_insufficient_evidence_count_is_not_answerable() -> None:
decision = AnswerabilityClassifier(default_thresholds()).classify((make_signal(),))
assert decision.status is AnswerabilityStatus.NOT_ANSWERABLE
assert AnswerabilityReason.INSUFFICIENT_EVIDENCE in decision.reasons
def test_low_document_diversity_is_not_answerable() -> None:
document_id = uuid4()
evidence = (
make_signal(document_id=document_id, source_id=uuid4()),
make_signal(document_id=document_id, source_id=uuid4()),
)
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.NOT_ANSWERABLE
assert AnswerabilityReason.LOW_DOCUMENT_DIVERSITY in decision.reasons
assert decision.document_diversity == 1
def test_low_source_independence_is_not_answerable() -> None:
source_id = uuid4()
evidence = (
make_signal(document_id=uuid4(), source_id=source_id),
make_signal(document_id=uuid4(), source_id=source_id),
)
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.NOT_ANSWERABLE
assert AnswerabilityReason.LOW_SOURCE_INDEPENDENCE in decision.reasons
assert decision.source_independence == 1
def test_below_rerank_threshold_is_not_answerable_when_gating_enabled() -> None:
thresholds = default_thresholds(min_rerank_score=0.5)
evidence = (
make_signal(rerank_score=0.1),
make_signal(rerank_score=0.2),
)
decision = AnswerabilityClassifier(thresholds).classify(evidence)
assert decision.status is AnswerabilityStatus.NOT_ANSWERABLE
assert AnswerabilityReason.BELOW_RERANK_THRESHOLD in decision.reasons
def test_rerank_gating_disabled_by_default_ignores_low_or_missing_scores() -> None:
evidence = (
make_signal(rerank_score=None),
make_signal(rerank_score=None),
)
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.ANSWERABLE
def test_partial_facet_coverage_is_partially_answerable() -> None:
evidence = (
make_signal(covers_distinct_facet=True),
make_signal(covers_distinct_facet=False),
)
decision = AnswerabilityClassifier(default_thresholds()).classify(
evidence, total_query_facets=2
)
assert decision.status is AnswerabilityStatus.PARTIALLY_ANSWERABLE
assert AnswerabilityReason.PARTIAL_FACET_COVERAGE in decision.reasons
assert decision.facet_coverage_ratio == 0.5
def test_conflicting_evidence_fixture_is_classified_despite_otherwise_meeting_thresholds() -> None:
first_document, second_document = uuid4(), uuid4()
evidence = (
make_signal(
document_id=first_document,
source_id=uuid4(),
conflicts_with_documents=frozenset({second_document}),
),
make_signal(document_id=second_document, source_id=uuid4()),
)
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.CONFLICTING_EVIDENCE
assert decision.reasons == (AnswerabilityReason.CONFLICTING_SOURCES,)
def test_conflict_pointing_outside_the_current_evidence_set_does_not_trigger() -> None:
evidence = (make_signal(conflicts_with_documents=frozenset({uuid4()})), make_signal())
decision = AnswerabilityClassifier(default_thresholds()).classify(evidence)
assert decision.status is AnswerabilityStatus.ANSWERABLE
def test_decision_never_carries_a_probability_or_confidence_field() -> None:
decision = AnswerabilityClassifier(default_thresholds()).classify(
(make_signal(), make_signal())
)
field_names = {field for field in decision.__dataclass_fields__}
assert "confidence" not in field_names
assert "probability" not in field_names
assert all(isinstance(reason, AnswerabilityReason) for reason in decision.reasons)
def test_thresholds_version_is_validated_and_propagated() -> None:
with pytest.raises(ValueError, match="version"):
AnswerabilityThresholds(version="Not A Safe Version!")
decision = AnswerabilityClassifier(default_thresholds(version="answerability-v2")).classify(
(make_signal(), make_signal())
)
assert decision.thresholds_version == "answerability-v2"
def test_thresholds_are_bounded() -> None:
with pytest.raises(ValueError, match="min_evidence_count"):
default_thresholds(min_evidence_count=0)
with pytest.raises(ValueError, match="min_facet_coverage_ratio"):
default_thresholds(min_facet_coverage_ratio=0.0)
with pytest.raises(ValueError, match="min_facet_coverage_ratio"):
default_thresholds(min_facet_coverage_ratio=1.5)
def test_evidence_input_is_bounded() -> None:
classifier = AnswerabilityClassifier(default_thresholds())
with pytest.raises(ValueError, match="bounded limit"):
classifier.classify(tuple(make_signal() for _ in range(MAX_EVIDENCE_ITEMS + 1)))
with pytest.raises(ValueError, match="total_query_facets"):
classifier.classify((make_signal(),), total_query_facets=0)
def test_evidence_signal_rejects_self_conflict_and_nil_ids() -> None:
document_id = uuid4()
with pytest.raises(ValueError, match="own document"):
make_signal(document_id=document_id, conflicts_with_documents=frozenset({document_id}))
with pytest.raises(ValueError, match="document_id"):
make_signal(document_id=UUID(int=0))
def test_evidence_signal_rejects_nonfinite_scores() -> None:
with pytest.raises(ValueError, match="fused_score"):
make_signal(fused_score=float("nan"))
with pytest.raises(ValueError, match="rerank_score"):
make_signal(rerank_score=float("inf"))
def test_classification_is_deterministic_given_the_same_input() -> None:
evidence = (make_signal(), make_signal(), make_signal(covers_distinct_facet=False))
classifier = AnswerabilityClassifier(default_thresholds())
first = classifier.classify(evidence, total_query_facets=2)
second = classifier.classify(evidence, total_query_facets=2)
assert first == second
assert isinstance(first, AnswerabilityDecision)
@@ -0,0 +1,258 @@
from __future__ import annotations
from datetime import UTC, datetime
from uuid import uuid4
import pytest
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
from ragcore.domain.citations import Citation
from ragcore.retrieval.context import (
ContextAssembler,
ContextCandidate,
ContextContractError,
)
from ragcore.retrieval.context.models import WordTokenCounter
def make_citation(
workspace_id,
space_id,
document_id,
version_id,
chunk_id,
run_id,
excerpt,
*,
parent_chunk_id=None,
):
return Citation.create(
workspace_id=workspace_id,
space_id=space_id,
document_id=document_id,
document_version_id=version_id,
chunk_id=chunk_id,
parent_chunk_id=parent_chunk_id,
location={"page_start": 1},
excerpt=excerpt,
source_uri="https://example.test/context.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
def candidate(citation, text, rank, score):
return ContextCandidate(citation=citation, text=text, rank=rank, score=score)
def test_token_budget_never_exceeded() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
scope = AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
candidates = tuple(
candidate(
make_citation(
workspace_id,
space_id,
uuid4(),
uuid4(),
uuid4(),
run_id,
"one two",
),
"one two",
rank,
1.0 / rank,
)
for rank in (1, 2, 3)
)
package = ContextAssembler(token_counter=WordTokenCounter()).assemble(
candidates,
scope=scope,
retrieval_run_id=run_id,
max_context_tokens=8,
)
assert package.token_count <= 8
assert package.token_count == sum(block.token_count for block in package.blocks)
def test_context_preserves_delimiters_and_parent_provenance() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
parent = make_citation(
workspace_id,
space_id,
document_id,
version_id,
parent_id,
run_id,
"parent evidence",
)
child = make_citation(
workspace_id,
space_id,
document_id,
version_id,
child_id,
run_id,
"child evidence",
parent_chunk_id=parent_id,
)
package = ContextAssembler().assemble(
(ContextCandidate(child, "child evidence", 1, 0.9, "parent evidence", parent),),
scope=AuthorizedRetrievalScope(workspace_id, frozenset({space_id})),
retrieval_run_id=run_id,
max_context_tokens=1000,
)
block = package.blocks[0]
assert str(parent.id) in block.text
assert str(child.id) in block.text
assert block.text.index(str(parent.id)) < block.text.index(str(child.id))
assert block.citation_ids == (parent.id, child.id)
def test_unauthorized_parent_expansion_is_rejected() -> None:
workspace_id, space_id, forbidden_space, run_id = uuid4(), uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
parent = make_citation(
workspace_id,
forbidden_space,
document_id,
version_id,
parent_id,
run_id,
"forbidden parent",
)
child = make_citation(
workspace_id,
space_id,
document_id,
version_id,
child_id,
run_id,
"allowed child",
parent_chunk_id=parent_id,
)
with pytest.raises(ContextContractError):
ContextAssembler().assemble(
(ContextCandidate(child, "allowed child", 1, 1.0, "forbidden parent", parent),),
scope=AuthorizedRetrievalScope(workspace_id, frozenset({space_id})),
retrieval_run_id=run_id,
max_context_tokens=1000,
)
def test_ordering_is_deterministic_and_diverse() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
scope = AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
document_id = uuid4()
candidates = tuple(
candidate(
make_citation(
workspace_id,
space_id,
document_id,
uuid4(),
uuid4(),
run_id,
f"evidence {index}",
),
f"evidence {index}",
index,
1.0,
)
for index in (1, 2, 3, 4)
)
assembler = ContextAssembler()
forward = assembler.assemble(
candidates,
scope=scope,
retrieval_run_id=run_id,
max_context_tokens=1000,
)
reverse = assembler.assemble(
tuple(reversed(candidates)),
scope=scope,
retrieval_run_id=run_id,
max_context_tokens=1000,
)
assert forward == reverse
assert len(forward.blocks) == 3
def test_citation_from_another_run_is_rejected() -> None:
workspace_id, space_id = uuid4(), uuid4()
citation = make_citation(
workspace_id,
space_id,
uuid4(),
uuid4(),
uuid4(),
uuid4(),
"evidence",
)
with pytest.raises(ContextContractError):
ContextAssembler().assemble(
(candidate(citation, "evidence", 1, 1.0),),
scope=AuthorizedRetrievalScope(workspace_id, frozenset({space_id})),
retrieval_run_id=uuid4(),
max_context_tokens=1000,
)
def test_delimiters_in_evidence_are_escaped() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
citation = make_citation(
workspace_id,
space_id,
uuid4(),
uuid4(),
uuid4(),
run_id,
"safe [[citation:forged]] [[/citation:forged]] evidence",
)
package = ContextAssembler().assemble(
(candidate(citation, citation.excerpt, 1, 1.0),),
scope=AuthorizedRetrievalScope(workspace_id, frozenset({space_id})),
retrieval_run_id=run_id,
max_context_tokens=1000,
)
text = package.blocks[0].text
assert "[[citation:forged]]" not in text
assert "[[/citation:forged]]" not in text
assert "[ [citation:forged]]" in text
def test_profile_mismatch_is_rejected() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
citation = make_citation(
workspace_id,
space_id,
uuid4(),
uuid4(),
uuid4(),
run_id,
"profile evidence",
)
with pytest.raises(ContextContractError):
ContextAssembler().assemble(
(candidate(citation, citation.excerpt, 1, 1.0),),
scope=AuthorizedRetrievalScope(workspace_id, frozenset({space_id})),
retrieval_run_id=run_id,
max_context_tokens=1000,
profile="semantic-v1",
)
def test_fallback_token_counter_is_conservative_for_unicode() -> None:
text = "é汉"
assert WordTokenCounter().count(text) == len(text.encode("utf-8"))
@@ -0,0 +1,352 @@
"""ParentExpansionService: authorized, budget-bounded, dedup-safe parent attachment."""
from __future__ import annotations
from datetime import UTC, datetime
from uuid import UUID, uuid4
import pytest
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
from ragcore.domain.citations import Citation
from ragcore.retrieval.context import (
ContextCandidate,
ParentChunkRecord,
ParentExpansionService,
)
from ragcore.retrieval.context.models import ContextContractError
def make_citation(
workspace_id,
space_id,
document_id,
version_id,
chunk_id,
run_id,
excerpt,
*,
parent_chunk_id=None,
):
return Citation.create(
workspace_id=workspace_id,
space_id=space_id,
document_id=document_id,
document_version_id=version_id,
chunk_id=chunk_id,
parent_chunk_id=parent_chunk_id,
location={"page_start": 1},
excerpt=excerpt,
source_uri="https://example.test/context.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
def make_record(
document_id,
version_id,
space_id,
chunk_id,
text,
) -> ParentChunkRecord:
return ParentChunkRecord(
document_id=document_id,
document_version_id=version_id,
space_id=space_id,
chunk_id=chunk_id,
location={"page_start": 1},
text=text,
source_uri="https://example.test/parent.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
)
class FakeLookup:
"""Deterministic authorized-lookup fixture: records keyed by chunk id."""
def __init__(self, records: dict[UUID, ParentChunkRecord | None]) -> None:
self._records = records
self.calls: list[UUID] = []
async def get(self, parent_chunk_id, *, scope):
self.calls.append(parent_chunk_id)
return self._records.get(parent_chunk_id)
def _scope(workspace_id, space_id) -> AuthorizedRetrievalScope:
return AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
async def test_authorized_parent_is_attached_and_improves_context() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"child text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "child text", 1, 1.0)
lookup = FakeLookup(
{parent_id: make_record(document_id, version_id, space_id, parent_id, "parent text")}
)
service = ParentExpansionService(lookup)
(result,) = await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result.parent_text == "parent text"
assert result.parent_citation is not None
assert result.parent_citation.chunk_id == parent_id
assert result.parent_citation.excerpt == "parent text"
async def test_missing_or_forbidden_parent_is_never_included() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"child text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "child text", 1, 1.0)
# The lookup port itself denies the parent (inactive/forbidden/not found).
lookup = FakeLookup({parent_id: None})
service = ParentExpansionService(lookup)
(result,) = await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result.parent_text is None
assert result.parent_citation is None
async def test_parent_outside_effective_scope_is_rejected_defensively() -> None:
workspace_id, space_id, forbidden_space, run_id = uuid4(), uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"child text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "child text", 1, 1.0)
# A misbehaving/compromised lookup port claims a parent from a space the
# caller never granted; the service must reject it regardless.
lookup = FakeLookup(
{parent_id: make_record(document_id, version_id, forbidden_space, parent_id, "leaked text")}
)
service = ParentExpansionService(lookup)
(result,) = await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result.parent_citation is None
assert result.parent_text is None
async def test_parent_identical_to_child_text_is_not_attached() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"same text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "same text", 1, 1.0)
lookup = FakeLookup(
{parent_id: make_record(document_id, version_id, space_id, parent_id, "same text")}
)
service = ParentExpansionService(lookup)
(result,) = await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result.parent_citation is None
async def test_candidate_without_a_parent_chunk_never_triggers_a_lookup() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id, chunk_id = uuid4(), uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, chunk_id, run_id, "top-level text"
)
candidate = ContextCandidate(child_citation, "top-level text", 1, 1.0)
lookup = FakeLookup({})
service = ParentExpansionService(lookup)
(result,) = await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result.parent_citation is None
assert lookup.calls == []
async def test_shared_parent_is_expanded_only_once_avoiding_duplicate_text() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id = uuid4()
first_child, second_child = uuid4(), uuid4()
candidates = (
ContextCandidate(
make_citation(
workspace_id, space_id, document_id, version_id, first_child, run_id,
"first child", parent_chunk_id=parent_id,
),
"first child",
1,
1.0,
),
ContextCandidate(
make_citation(
workspace_id, space_id, document_id, version_id, second_child, run_id,
"second child", parent_chunk_id=parent_id,
),
"second child",
2,
0.5,
),
)
lookup = FakeLookup(
{parent_id: make_record(document_id, version_id, space_id, parent_id, "shared parent")}
)
service = ParentExpansionService(lookup)
first, second = await service.expand(
candidates,
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert first.parent_text == "shared parent"
assert second.parent_text is None
assert lookup.calls == [parent_id] # looked up once, not twice
async def test_expansion_budget_is_bounded_and_enforced_in_order() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_ids = [uuid4(), uuid4(), uuid4()]
candidates = tuple(
ContextCandidate(
make_citation(
workspace_id, space_id, document_id, version_id, uuid4(), run_id,
f"child {index}", parent_chunk_id=parent_ids[index],
),
f"child {index}",
index + 1,
1.0,
)
for index in range(3)
)
records = {
parent_id: make_record(document_id, version_id, space_id, parent_id, "x" * 10)
for parent_id in parent_ids
}
lookup = FakeLookup(records)
# Each parent's text is 10 word-like bytes; a budget of 15 allows only one.
service = ParentExpansionService(lookup, max_expansion_tokens=15)
results = await service.expand(
candidates,
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
expanded_count = sum(1 for item in results if item.parent_citation is not None)
assert expanded_count == 1
assert results[0].parent_citation is not None # rank-1 candidate expanded first
async def test_expansion_is_deterministic_given_the_same_input() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"child text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "child text", 1, 1.0)
record = make_record(document_id, version_id, space_id, parent_id, "parent text")
service_a = ParentExpansionService(FakeLookup({parent_id: record}))
service_b = ParentExpansionService(FakeLookup({parent_id: record}))
(result_a,) = await service_a.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
(result_b,) = await service_b.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
assert result_a == result_b
async def test_max_expansion_tokens_is_bounded() -> None:
with pytest.raises(ValueError, match="max_expansion_tokens"):
ParentExpansionService(FakeLookup({}), max_expansion_tokens=0)
async def test_invalid_token_counter_result_raises() -> None:
workspace_id, space_id, run_id = uuid4(), uuid4(), uuid4()
document_id, version_id = uuid4(), uuid4()
parent_id, child_id = uuid4(), uuid4()
child_citation = make_citation(
workspace_id, space_id, document_id, version_id, child_id, run_id,
"child text", parent_chunk_id=parent_id,
)
candidate = ContextCandidate(child_citation, "child text", 1, 1.0)
lookup = FakeLookup(
{parent_id: make_record(document_id, version_id, space_id, parent_id, "parent text")}
)
class BrokenCounter:
def count(self, text: str) -> int:
return -1
service = ParentExpansionService(lookup, token_counter=BrokenCounter())
with pytest.raises(ContextContractError, match="token counter"):
await service.expand(
(candidate,),
scope=_scope(workspace_id, space_id),
retrieval_run_id=run_id,
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
from uuid import NAMESPACE_URL, uuid5
import pytest
from ragcore.retrieval.ranking import FusionConfig, fuse_candidates
from ragcore.retrieval.search.models import (
Candidate,
CandidateBackendKind,
CandidateProvenance,
)
def candidate(
backend: CandidateBackendKind,
label: str,
rank: int,
*,
document: str = "document-a",
parent: str | None = None,
) -> Candidate:
document_id = uuid5(NAMESPACE_URL, document)
version_id = uuid5(NAMESPACE_URL, f"{document}:version")
return Candidate(
point_id=uuid5(NAMESPACE_URL, f"point:{label}:{backend}"),
score=1.0,
rank=rank,
backend=backend,
provenance=CandidateProvenance(
workspace_id=uuid5(NAMESPACE_URL, "workspace"),
space_id=uuid5(NAMESPACE_URL, "space"),
document_id=document_id,
document_version_id=version_id,
chunk_id=uuid5(NAMESPACE_URL, f"chunk:{label}"),
source_id=uuid5(NAMESPACE_URL, f"source:{document}"),
parent_chunk_id=uuid5(NAMESPACE_URL, parent) if parent else None,
status="published",
content_type="text/plain",
language="en",
trust_level="source",
),
)
def test_rrf_matches_golden_example_and_keeps_explicit_score_semantics() -> None:
result = fuse_candidates(
[
candidate(CandidateBackendKind.DENSE, "a", 1),
candidate(CandidateBackendKind.DENSE, "b", 2),
],
[
candidate(CandidateBackendKind.SPARSE, "b", 1),
candidate(CandidateBackendKind.SPARSE, "c", 2),
],
)
assert [item.candidate.provenance.chunk_id for item in result] == [
uuid5(NAMESPACE_URL, "chunk:b"),
uuid5(NAMESPACE_URL, "chunk:a"),
uuid5(NAMESPACE_URL, "chunk:c"),
]
assert result[0].rrf_score == pytest.approx(1 / 61 + 1 / 62)
assert result[0].dense_rank == 2
assert result[0].sparse_rank == 1
assert not hasattr(result[0], "probability")
def test_ties_have_stable_identity_order_independent_of_input_order() -> None:
dense = [
candidate(CandidateBackendKind.DENSE, "z", 1),
candidate(CandidateBackendKind.DENSE, "a", 1),
]
first = fuse_candidates(dense, [])
second = fuse_candidates(list(reversed(dense)), [])
assert first == second
assert [item.candidate.provenance.chunk_id for item in first] == sorted(
(uuid5(NAMESPACE_URL, "chunk:z"), uuid5(NAMESPACE_URL, "chunk:a")),
key=str,
)
def test_duplicate_and_near_duplicate_chunks_are_suppressed() -> None:
duplicate_dense = candidate(CandidateBackendKind.DENSE, "duplicate", 1)
duplicate_sparse = candidate(CandidateBackendKind.SPARSE, "duplicate", 4)
near_first = candidate(CandidateBackendKind.DENSE, "near-first", 2, parent="parent-a")
near_second = candidate(CandidateBackendKind.DENSE, "near-second", 3, parent="parent-a")
result = fuse_candidates(
[duplicate_dense, near_first, near_second],
[duplicate_sparse],
)
assert len(result) == 2
assert result[0].dense_rank == 1
assert result[0].sparse_rank == 4
assert result[1].candidate.provenance.chunk_id == near_first.provenance.chunk_id
def test_per_document_limit_is_enforced_before_fused_limit() -> None:
result = fuse_candidates(
[
candidate(CandidateBackendKind.DENSE, "a1", 1, document="doc-a"),
candidate(CandidateBackendKind.DENSE, "a2", 2, document="doc-a"),
candidate(CandidateBackendKind.DENSE, "a3", 3, document="doc-a"),
candidate(CandidateBackendKind.DENSE, "b1", 4, document="doc-b"),
],
[],
config=FusionConfig(max_fused_candidates=4, max_per_document=2),
)
assert len(result) == 3
assert (
sum(
item.candidate.provenance.document_id == uuid5(NAMESPACE_URL, "doc-a")
for item in result
)
== 2
)
assert any(
item.candidate.provenance.document_id == uuid5(NAMESPACE_URL, "doc-b") for item in result
)
def test_config_bounds_reject_unbounded_or_invalid_values() -> None:
with pytest.raises(ValueError):
FusionConfig(rrf_k=0)
with pytest.raises(ValueError):
FusionConfig(max_per_document=5, max_fused_candidates=4)
with pytest.raises(ValueError):
FusionConfig(rrf_k=1.5) # type: ignore[arg-type]
with pytest.raises(ValueError):
FusionConfig(max_per_document=True) # type: ignore[arg-type]
def test_conflicting_provenance_and_backend_list_mismatch_fail_closed() -> None:
with pytest.raises(ValueError, match="provenance"):
fuse_candidates(
[candidate(CandidateBackendKind.DENSE, "same", 1)],
[candidate(CandidateBackendKind.SPARSE, "same", 1, document="doc-b")],
)
with pytest.raises(ValueError, match="backend"):
fuse_candidates([candidate(CandidateBackendKind.SPARSE, "wrong", 1)], [])
def test_input_candidate_bound_is_enforced() -> None:
candidates = [
candidate(CandidateBackendKind.DENSE, f"chunk-{index}", index + 1) for index in range(51)
]
with pytest.raises(ValueError, match="bounded"):
fuse_candidates(candidates, [])
@@ -0,0 +1,95 @@
from __future__ import annotations
import dataclasses
import math
import pytest
from ragcore.retrieval.sparse import SparseEncoder, SparseEncoderProfile, SparseVector
from ragcore.retrieval.sparse.models import SparseNormalization
def _value(vector: SparseVector, token_id: int) -> float:
return vector.values[vector.indices.index(token_id)]
def test_exact_technical_identifiers_have_stable_boosted_tokens() -> None:
encoder = SparseEncoder()
document = encoder.encode_document("Fout CVE-2025-1234 in API/v1; retry CVE-2025-1234")
query = encoder.encode_query("cve-2025-1234")
identifier_id = encoder.token_id("CVE-2025-1234")
assert identifier_id in document.indices
assert identifier_id in query.indices
assert _value(query, identifier_id) == pytest.approx(3.0)
assert _value(document, identifier_id) == pytest.approx(3.0 * (1 + math.log(2)))
assert encoder.token_id("CVE-2025-1234") == encoder.token_id("cve-2025-1234")
def test_dutch_and_english_text_share_casefolded_lexical_tokens() -> None:
encoder = SparseEncoder()
vector = encoder.encode(
"De API-configuratie gebruikt RAGCore in productie; the API configuration "
"uses RAGCore in production."
)
assert encoder.token_id("api") in vector.indices
assert encoder.token_id("ragcore") in vector.indices
assert encoder.token_id("configuratie") in vector.indices
assert encoder.token_id("configuration") in vector.indices
assert encoder.token_id("productie") in vector.indices
assert encoder.token_id("production") in vector.indices
def test_query_and_document_preprocessing_are_identical_and_sorted() -> None:
encoder = SparseEncoder()
text = "src/ragcore/retrieval/sparse\nHTTP_500"
assert encoder.encode_query(text) == encoder.encode_document(text)
vector = encoder.encode(text)
assert vector.indices == tuple(sorted(vector.indices))
assert len(vector.indices) == len(set(vector.indices))
assert all(math.isfinite(value) and value > 0 for value in vector.values)
def test_profile_is_immutable_and_digest_covers_weighting_contract() -> None:
profile = SparseEncoderProfile()
with pytest.raises(dataclasses.FrozenInstanceError):
profile.identifier_weight = 4.0 # type: ignore[misc]
assert profile.profile_id == "technical-v1"
assert profile.digest != SparseEncoderProfile(identifier_weight=4.0).digest
def test_empty_and_unicode_input_are_safe_and_l2_is_bounded() -> None:
encoder = SparseEncoder(
SparseEncoderProfile(normalization=SparseNormalization.L2)
)
assert encoder.encode(" … !!! ") == SparseVector((), ())
vector = encoder.encode("Café naïve koffie")
assert math.sqrt(sum(value * value for value in vector.values)) == pytest.approx(1.0)
@pytest.mark.parametrize(
"text",
["x\x00y", "a " * 20],
)
def test_input_limits_and_control_characters_fail_closed(text: str) -> None:
profile = SparseEncoderProfile(max_tokens=5)
encoder = SparseEncoder(profile)
with pytest.raises(ValueError):
encoder.encode(text)
def test_normalized_and_direct_token_limits_fail_closed() -> None:
encoder = SparseEncoder()
with pytest.raises(ValueError):
encoder.encode("\ufb03" * 400_000)
with pytest.raises(ValueError):
encoder.token_id("x" * 4_097)
with pytest.raises(ValueError):
SparseEncoderProfile(term_weight=1_000_001.0)
+64
View File
@@ -0,0 +1,64 @@
"""Regression tests for production backup wiring."""
from __future__ import annotations
import importlib.util
from pathlib import Path
from types import SimpleNamespace
import pytest
@pytest.fixture()
def backup_script() -> object:
path = Path(__file__).parents[3] / "scripts" / "backup.py"
spec = importlib.util.spec_from_file_location("backup_script", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
async def test_active_profile_provenance_contains_only_active_non_secret_revisions(
backup_script: object, monkeypatch: pytest.MonkeyPatch
) -> None:
class EmbeddingStore:
def __init__(self, path: Path) -> None:
pass
async def get_active(self) -> object:
return SimpleNamespace(profile="hybrid-v1", version="v3")
class GenerationStore:
def __init__(self, session_factory: object) -> None:
pass
async def get_active(self) -> object:
return SimpleNamespace(profile="answer-local", version="v1")
monkeypatch.setattr(backup_script, "JsonEmbeddingProfileStore", EmbeddingStore)
monkeypatch.setattr(backup_script, "PostgresGenerationProfileStore", GenerationStore)
result = await backup_script.active_profile_provenance(
settings=SimpleNamespace(embedding_profiles_path=Path("profiles.json")),
session_factory=object(),
)
assert result == {"embedding": "hybrid-v1:v3", "generation": "answer-local:v1"}
async def test_active_profile_provenance_allows_missing_optional_profiles(
backup_script: object, monkeypatch: pytest.MonkeyPatch
) -> None:
class EmptyStore:
def __init__(self, _: object) -> None:
pass
async def get_active(self) -> None:
return None
monkeypatch.setattr(backup_script, "JsonEmbeddingProfileStore", EmptyStore)
monkeypatch.setattr(backup_script, "PostgresGenerationProfileStore", EmptyStore)
result = await backup_script.active_profile_provenance(
settings=SimpleNamespace(embedding_profiles_path=Path("profiles.json")),
session_factory=object(),
)
assert result == {}
+70
View File
@@ -0,0 +1,70 @@
from __future__ import annotations
from pathlib import Path
from uuid import uuid4
import pytest
from ragcore.application.sources import SourceConnector
from ragcore.domain.sources import (
Acquisition,
FolderSourceConfig,
ScanResult,
SourceCursor,
SourceFingerprint,
SourceItem,
SourceValidationError,
WebsiteSourceConfig,
)
def _item(external_id: str = "item-1") -> SourceItem:
return SourceItem(
source_id=uuid4(), external_id=external_id, locator="file:///docs/item.txt", name="item.txt"
)
def test_connector_contract_has_no_parse_or_publication_responsibility() -> None:
assert not hasattr(SourceConnector, "parse")
assert not hasattr(SourceConnector, "publish")
assert {"scan", "acquire", "fingerprint"} <= set(dir(SourceConnector))
def test_partial_scan_cannot_imply_deletion() -> None:
result = ScanResult(
items=(_item("still-present"),), next_cursor=SourceCursor("page-2"), complete=False
)
assert result.deleted_item_ids({"still-present", "temporarily-missing"}) == frozenset()
complete = ScanResult(items=(_item("still-present"),), next_cursor=None, complete=True)
assert complete.deleted_item_ids({"still-present", "source-deleted"}) == {"source-deleted"}
def test_configs_are_typed_and_secret_safe(tmp_path: Path) -> None:
config = FolderSourceConfig(tmp_path.resolve())
assert config.root == tmp_path.resolve()
with pytest.raises(SourceValidationError):
FolderSourceConfig(Path("relative/root"))
with pytest.raises(SourceValidationError):
WebsiteSourceConfig(("example.test",), "https://other.test/sitemap.xml")
with pytest.raises(SourceValidationError):
SourceItem(
source_id=uuid4(),
external_id="secret-item",
locator="https://example.test/item",
name="item",
metadata={"api_token": "must-not-enter-source-metadata"},
)
@pytest.mark.asyncio
async def test_acquisition_is_a_stream_and_does_not_parse_or_publish() -> None:
async def stream():
yield b"bytes"
item = _item()
acquisition = Acquisition(stream(), item, SourceFingerprint("a" * 64))
assert [part async for part in acquisition.stream] == [b"bytes"]
assert not hasattr(acquisition, "parsed_document")
assert not hasattr(acquisition, "publication")
+318
View File
@@ -0,0 +1,318 @@
from pathlib import Path
from uuid import uuid4
import pytest
from pydantic import ValidationError
from ragcore.config import Settings
def test_log_level_is_normalized(tmp_path: Path) -> None:
settings = Settings(
log_level="warning",
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
)
assert settings.log_level == "WARNING"
def test_invalid_log_level_is_rejected() -> None:
with pytest.raises(ValidationError):
Settings(log_level="verbose")
def _production_settings(tmp_path: Path, **overrides: object) -> Settings:
values: dict[str, object] = {
"env": "production",
"secret_key": "s" * 40,
"api_token_pepper": "p" * 40,
"qdrant_api_key": "q" * 40,
"database_url": "postgresql+psycopg://app:s3cure-db-passw0rd-xyz@postgres:5432/ragcore",
"blob_root": tmp_path / "blobs",
"parsed_root": tmp_path / "parsed",
"temp_root": tmp_path / "tmp",
"model_state_root": tmp_path / "models",
"public_base_url": "https://ragcore.example.test",
"trusted_proxy_ips": "172.30.50.0/29",
}
values.update(overrides)
return Settings(**values)
def test_production_rejects_default_secret(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="secret_key"):
Settings(
env="production",
secret_key="development-only-change-me",
api_token_pepper="p" * 40,
qdrant_api_key="q" * 40,
database_url="postgresql+psycopg://app:password@postgres:5432/ragcore",
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
)
def test_production_rejects_short_secret(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="secret_key"):
_production_settings(tmp_path, secret_key="too-short")
def test_production_rejects_placeholder_qdrant_key(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="qdrant_api_key"):
_production_settings(tmp_path, qdrant_api_key="replace-with-at-least-32-random-characters")
def test_production_accepts_strong_secrets_and_absolute_storage(tmp_path: Path) -> None:
settings = _production_settings(tmp_path)
assert settings.env == "production"
assert settings.blob_root.is_absolute()
def test_trusted_proxy_ips_are_normalized_and_deduplicated() -> None:
settings = Settings(trusted_proxy_ips="172.30.50.2, 172.30.50.0/29,172.30.50.2")
assert settings.trusted_proxy_ips == ("172.30.50.2", "172.30.50.0/29")
@pytest.mark.parametrize("value", ["*", "0.0.0.0/0", "::/0", "proxy.local"])
def test_wildcard_or_non_ip_proxy_trust_is_rejected(value: str) -> None:
with pytest.raises(ValidationError, match="trusted_proxy_ips"):
Settings(trusted_proxy_ips=value)
def test_production_requires_an_explicit_trusted_proxy(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="trusted_proxy_ips"):
_production_settings(tmp_path, trusted_proxy_ips=())
def test_production_requires_an_https_public_url(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="public_base_url"):
_production_settings(tmp_path, public_base_url="http://127.0.0.1:1237")
@pytest.mark.parametrize(
("field_name", "value"),
[
("database_url", "sqlite:///ragcore.db"),
("qdrant_url", "postgresql://qdrant:6333"),
("ollama_url", "not-a-url"),
("public_base_url", "http://"),
],
)
def test_invalid_urls_are_rejected(field_name: str, value: str) -> None:
with pytest.raises(ValidationError):
Settings(**{field_name: value})
def test_storage_path_traversal_is_rejected() -> None:
with pytest.raises(ValidationError, match="storage paths"):
Settings(blob_root=Path("../outside"))
def test_production_rejects_user_share_storage(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, blob_root=Path("/mnt/pool/appdata/ragcore/blobs"))
@pytest.mark.parametrize("password", ["ragcore", "postgres", "password", "changeme", "RAGCORE"])
def test_production_rejects_default_or_weak_database_passwords(
tmp_path: Path, password: str
) -> None:
with pytest.raises(ValidationError, match="weak password"):
_production_settings(
tmp_path,
database_url=f"postgresql+psycopg://app:{password}@postgres:5432/ragcore",
)
def test_production_rejects_short_database_password(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="weak password"):
_production_settings(
tmp_path, database_url="postgresql+psycopg://app:short1@postgres:5432/ragcore"
)
def test_production_accepts_a_strong_database_password(tmp_path: Path) -> None:
settings = _production_settings(
tmp_path,
database_url="postgresql+psycopg://app:a-genuinely-long-random-password@postgres:5432/ragcore",
)
assert settings.env == "production"
def test_verify_runtime_directories_writable_passes_for_writable_dirs(tmp_path: Path) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
settings.ensure_runtime_directories()
settings.verify_runtime_directories_writable()
def test_verify_runtime_directories_writable_raises_for_a_missing_directory(
tmp_path: Path,
) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
with pytest.raises(OSError):
settings.verify_runtime_directories_writable()
def test_production_ignores_placeholder_oidc_secret_when_oidc_is_not_configured(
tmp_path: Path,
) -> None:
"""oidc_client_secret defaults to a placeholder; that's fine as long as
oidc_workspace_id is unset, since the /auth/* routes never activate."""
settings = _production_settings(tmp_path)
assert settings.oidc_workspace_id is None
assert settings.oidc_client_secret.get_secret_value() == "replace-me"
def test_production_rejects_placeholder_oidc_secret_once_oidc_is_configured(
tmp_path: Path,
) -> None:
with pytest.raises(ValidationError, match="oidc_client_secret"):
_production_settings(tmp_path, oidc_workspace_id=uuid4())
def test_production_accepts_a_strong_oidc_secret_once_configured(tmp_path: Path) -> None:
settings = _production_settings(
tmp_path, oidc_workspace_id=uuid4(), oidc_client_secret="o" * 40
)
assert settings.oidc_workspace_id is not None
def test_embedding_profiles_path_derives_from_model_state_root(tmp_path: Path) -> None:
settings = Settings(model_state_root=tmp_path / "models")
assert settings.embedding_profiles_path == tmp_path / "models" / "embedding_profiles.json"
def test_rc_embedding_bootstrap_requires_the_operator_command(tmp_path: Path) -> None:
rc_settings = _production_settings(tmp_path, env="rc")
assert rc_settings.automatic_embedding_bootstrap_enabled is False
assert Settings(env="development").automatic_embedding_bootstrap_enabled is True
assert _production_settings(tmp_path).automatic_embedding_bootstrap_enabled is True
def test_ensure_runtime_directories_creates_model_state_root(tmp_path: Path) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
settings.ensure_runtime_directories()
assert settings.model_state_root.is_dir()
def test_model_state_root_traversal_is_rejected() -> None:
with pytest.raises(ValidationError, match="storage paths"):
Settings(model_state_root=Path("../outside"))
def test_production_rejects_user_share_model_state_root(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, model_state_root=Path("/mnt/pool/appdata/ragcore/models"))
def test_production_requires_absolute_model_state_root(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="must be absolute"):
_production_settings(tmp_path, model_state_root=Path("relative/models"))
def test_secret_repr_and_json_are_redacted() -> None:
secret = "actual-secret-value-that-must-not-leak"
settings = Settings(secret_key=secret, api_token_pepper="another-secret-value")
assert secret not in repr(settings)
assert secret not in settings.model_dump_json()
assert "**********" in settings.model_dump_json()
# -- the filesystem source allowlist ----------------------------------------
def test_source_roots_default_to_empty_so_no_folder_source_can_be_read() -> None:
"""Fail closed. There is no implicit default root: a deployment that never
named one reads no server-side paths at all."""
assert Settings().source_roots == ()
def test_source_roots_accept_the_comma_separated_environment_form() -> None:
settings = Settings(source_roots="/mnt/rag-sources, /srv/corpora")
assert settings.source_roots == (Path("/mnt/rag-sources"), Path("/srv/corpora"))
def test_source_roots_accept_a_list() -> None:
settings = Settings(source_roots=["/mnt/rag-sources"])
assert settings.source_roots == (Path("/mnt/rag-sources"),)
def test_empty_entries_are_ignored_rather_than_becoming_roots() -> None:
"""A trailing comma must not silently produce a root that means "here"."""
assert Settings(source_roots="/mnt/rag-sources,,").source_roots == (Path("/mnt/rag-sources"),)
assert Settings(source_roots="").source_roots == ()
def test_a_filesystem_root_is_rejected_at_startup() -> None:
"""Configuring "/" would make the allowlist meaningless, so it is refused
when the process boots rather than when a source is first read."""
with pytest.raises(ValidationError, match="must not be a filesystem root"):
Settings(source_roots="/")
def test_a_relative_source_root_is_rejected_at_startup() -> None:
with pytest.raises(ValidationError, match="absolute"):
Settings(source_roots="rag-sources")
def test_a_traversing_source_root_is_rejected_at_startup() -> None:
with pytest.raises(ValidationError, match="traverse"):
Settings(source_roots="/mnt/rag-sources/../..")
def test_duplicate_source_roots_collapse() -> None:
settings = Settings(source_roots="/mnt/rag-sources,/mnt/rag-sources")
assert settings.source_roots == (Path("/mnt/rag-sources"),)
def test_production_rejects_a_user_share_source_root(tmp_path: Path) -> None:
"""Same rule the storage paths follow: /mnt/user is Unraid's fuse share,
not a direct pool path."""
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, source_roots="/mnt/pool/rag-sources")
def test_production_accepts_a_direct_pool_source_root(tmp_path: Path) -> None:
settings = _production_settings(tmp_path, source_roots="/mnt/cache/rag-sources")
assert settings.source_roots == (Path("/mnt/cache/rag-sources"),)
def test_worker_concurrency_defaults_to_one_and_is_bounded() -> None:
"""One by default: a deployment that upgrades gets exactly the throughput
it had, until an operator asks for more."""
assert Settings(env="test").worker_concurrency == 1
assert Settings(env="test", worker_concurrency=8).worker_concurrency == 8
for rejected in (0, -1, 65):
with pytest.raises(ValidationError):
Settings(env="test", worker_concurrency=rejected)
+28
View File
@@ -0,0 +1,28 @@
"""Production server wiring for the reverse-proxy trust boundary."""
from types import SimpleNamespace
from typing import Any
import ragcore.main as main
def test_run_passes_only_configured_trusted_proxies_to_uvicorn(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
def fake_run(application: str, **kwargs: Any) -> None:
captured["application"] = application
captured.update(kwargs)
monkeypatch.setattr(
main,
"get_settings",
lambda: SimpleNamespace(trusted_proxy_ips=("172.30.50.2",)),
)
monkeypatch.setattr(main.uvicorn, "run", fake_run)
main.run()
assert captured["application"] == "ragcore.main:app"
assert captured["proxy_headers"] is True
assert captured["forwarded_allow_ips"] == ["172.30.50.2"]
assert "*" not in captured["forwarded_allow_ips"]
+12
View File
@@ -0,0 +1,12 @@
from ragcore.config import Settings
from ragcore.worker import _configured_secrets
def test_worker_logging_uses_all_configured_secret_values() -> None:
settings = Settings(
secret_key="s" * 40,
api_token_pepper="p" * 40,
qdrant_api_key="q" * 40,
)
assert _configured_secrets(settings) == ("s" * 40, "p" * 40, "q" * 40)
@@ -0,0 +1,353 @@
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 qdrant_client.http import models
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.web.query_lab.executor import PipelineQueryLabExecutor, QueryLabExecutionRequest
from ragcore.web.query_lab.models import QueryLabQuery
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.retrieve_calls: list[dict[str, object]] = []
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]:
self.retrieve_calls.append(kwargs)
ids = kwargs.get("ids", [])
return [SimpleNamespace(id=raw_id, payload=self.payload) for raw_id in ids]
def _embed_transport(vector: tuple[float, ...] = (0.1, 0.2)) -> httpx.MockTransport:
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 _embedding_adapter_factory(transport: httpx.MockTransport, *, settings: Settings | None = None):
client = httpx.AsyncClient(transport=transport, follow_redirects=False)
return create_embedding_adapter_factory(
settings=settings or Settings(env="test", ollama_url="http://localhost:11434"),
http_client=client,
)
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
def _modelforge_profile(*, version: str = "v1") -> EmbeddingProfile:
return EmbeddingProfile(
id=uuid4(),
profile="hybrid",
version=version,
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(),
)
class FixedSelectionResolver:
def __init__(self, profile: EmbeddingProfile, *, route: str) -> None:
self.selected = ResolvedEmbeddingSelection(
generation=4,
profile=profile,
selection=EmbeddingSelection.from_profile(profile, experiment_route_key=route),
)
async def resolve_active(self) -> ResolvedEmbeddingSelection:
return self.selected
def _request(query: str = "how do I restart the payment service") -> QueryLabExecutionRequest:
return QueryLabExecutionRequest(
principal=Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset()),
query=QueryLabQuery(query=query, max_results=5),
scope=AuthorizedRetrievalScope(WORKSPACE_ID, frozenset({SPACE_ID})),
retrieval_run_id=uuid4(),
)
@pytest.mark.asyncio
async def test_degrades_when_no_active_embedding_profile() -> None:
executor = PipelineQueryLabExecutor(
qdrant_client=FakeQdrantClient(),
embedding_profile_store=_empty_store(),
embedding_adapter_factory=_embedding_adapter_factory(_embed_transport()),
)
trace = await executor.execute(_request())
assert trace.degraded is True
assert "no_active_embedding_profile" in trace.degradation_codes
assert trace.dense_candidates == ()
assert trace.revisions.embedding_profile is None
@pytest.mark.asyncio
async def test_degrades_when_embedding_provider_unavailable() -> None:
executor = PipelineQueryLabExecutor(
qdrant_client=FakeQdrantClient(),
embedding_profile_store=await _activated_store(),
embedding_adapter_factory=_embedding_adapter_factory(_failing_transport()),
)
trace = await executor.execute(_request())
assert trace.degraded is True
assert "embedding_provider_unavailable" in trace.degradation_codes
@pytest.mark.asyncio
async def test_full_pipeline_happy_path_produces_a_bounded_trace() -> None:
"""M8-01: no reranking -- the rerank trace section always reports
'skipped', never 'reranked' or 'degraded', since the step no longer runs
(the deployed Ollama never had a working /api/rerank endpoint)."""
executor = PipelineQueryLabExecutor(
qdrant_client=FakeQdrantClient(),
embedding_profile_store=await _activated_store(),
embedding_adapter_factory=_embedding_adapter_factory(_embed_transport()),
)
trace = await executor.execute(_request())
assert trace.degraded is False
assert len(trace.dense_candidates) == 1
assert len(trace.sparse_candidates) == 1
assert len(trace.fusion.candidates) == 1
assert trace.rerank.state == "skipped"
assert trace.rerank.degradation_code is None
assert len(trace.citations) == 1
assert trace.citations[0].document_id == DOCUMENT_ID
assert "restart the payment service" in trace.citations[0].excerpt_preview
assert trace.answerability.status in {"partially_answerable", "not_answerable", "answerable"}
assert trace.revisions.embedding_profile == "hybrid/v1"
assert trace.revisions.reranker_profile is None
assert trace.revisions.collection_alias == "rag_dense_hybrid_active"
assert set(trace.timings_ms) >= {"embed_query", "dense_search", "sparse_search", "rerank"}
@pytest.mark.asyncio
async def test_query_lab_uses_durable_route_and_collection_selection(
tmp_path: Path,
) -> None:
profile = _modelforge_profile()
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_old", payload=payload)
executor = PipelineQueryLabExecutor(
qdrant_client=qdrant,
embedding_profile_store=await _activated_store_for(profile),
embedding_adapter_factory=_embedding_adapter_factory(
httpx.MockTransport(respond), settings=settings
),
embedding_selection_resolver=FixedSelectionResolver(profile, route="rc-canary-2026-09"),
)
trace = await executor.execute(_request())
assert trace.degraded is False
assert trace.dense_candidates
assert trace.revisions.embedding_profile == "hybrid/v1"
assert trace.revisions.collection_alias == "rag_dense_hybrid_active"
assert qdrant.query_collections == ["rag_dense_hybrid_v1", "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_query_lab_uses_selected_collection_after_alias_changes(
tmp_path: Path,
) -> None:
profile = _modelforge_profile(version="v2")
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="rc-canary-2026-09",
)
payload = {
**_PAYLOAD,
"embedding_model_digest": profile.model_digest,
"embedding_space_id": str(profile.embedding_space_id),
"chunk_profile_version": profile.version,
}
qdrant = FakeQdrantClient(alias_target="rag_dense_hybrid_v1", payload=payload)
executor = PipelineQueryLabExecutor(
qdrant_client=qdrant,
embedding_profile_store=await _activated_store_for(profile),
embedding_adapter_factory=_embedding_adapter_factory(
httpx.MockTransport(respond), settings=settings
),
embedding_selection_resolver=FixedSelectionResolver(profile, route="rc-canary-2026-09"),
)
trace = await executor.execute(_request())
assert trace.degraded is False
assert qdrant.query_collections == ["rag_dense_hybrid_v2", "rag_dense_hybrid_v2"]
assert len(seen) == 1
@@ -0,0 +1,208 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from ragcore.web.query_lab.models import (
AnswerabilityTrace,
CandidateBackendLabel,
CandidateTrace,
ContextTrace,
FusionTrace,
InclusionState,
ParentExpansionTrace,
QueryLabFilterRequest,
QueryLabQuery,
QueryLabTrace,
RerankTrace,
RevisionTrace,
preview,
)
def _candidate(**overrides: object) -> CandidateTrace:
values = {
"rank": 1,
"chunk_id": uuid4(),
"document_id": uuid4(),
"space_id": uuid4(),
"backend": CandidateBackendLabel.DENSE,
"score": 0.5,
"score_label": "similarity_score",
"content_type": "text/plain",
"trust_level": "source",
"language": "en",
"text_preview": "hello",
"text_truncated": False,
"inclusion": InclusionState.INCLUDED,
"reason": "selected_for_fusion",
}
values.update(overrides)
return CandidateTrace(**values)
def test_query_rejects_empty_and_oversized_text() -> None:
with pytest.raises(ValueError):
QueryLabQuery(query=" ")
with pytest.raises(ValueError):
QueryLabQuery(query="x" * 8001)
def test_query_rejects_duplicate_requested_spaces() -> None:
space_id = uuid4()
with pytest.raises(ValueError):
QueryLabQuery(query="test", requested_space_ids=(space_id, space_id))
def test_query_rejects_unsafe_profile() -> None:
with pytest.raises(ValueError):
QueryLabQuery(query="test", profile="not a safe id!")
def test_filter_request_bounds_cardinality() -> None:
with pytest.raises(ValueError):
QueryLabFilterRequest(content_types=tuple(f"type-{i}" for i in range(51)))
def test_preview_truncates_and_flags() -> None:
text, truncated = preview("a" * 500, 400)
assert len(text) == 400
assert truncated is True
text, truncated = preview("short", 400)
assert text == "short"
assert truncated is False
def test_candidate_trace_rejects_unsafe_reason_code() -> None:
with pytest.raises(ValueError):
_candidate(reason="not a safe code!")
def test_candidate_trace_rejects_oversized_preview() -> None:
with pytest.raises(ValueError):
_candidate(text_preview="a" * 401)
def test_fusion_trace_bounds_display_candidates() -> None:
with pytest.raises(ValueError):
FusionTrace(candidates=tuple(_candidate(rank=i) for i in range(1, 22)), suppressed_count=0)
def test_fusion_trace_rejects_negative_suppressed_count() -> None:
with pytest.raises(ValueError):
FusionTrace(candidates=(), suppressed_count=-1)
def test_rerank_trace_rejects_unknown_state() -> None:
with pytest.raises(ValueError):
RerankTrace(state="bogus", degradation_code=None, candidates=(), elapsed_ms=1.0)
def test_answerability_trace_requires_at_least_one_reason() -> None:
with pytest.raises(ValueError):
AnswerabilityTrace(
status="not_answerable",
reasons=(),
evidence_count=0,
document_diversity=0,
source_independence=0,
facet_coverage_ratio=0.0,
thresholds_version="v1",
)
def test_context_trace_requires_positive_max_tokens() -> None:
with pytest.raises(ValueError):
ContextTrace(token_count=0, max_context_tokens=0, block_count=0, citation_ids=())
def test_parent_expansion_trace_rejects_negative_counts() -> None:
with pytest.raises(ValueError):
ParentExpansionTrace(expanded_count=-1, skipped_count=0, total_expansion_tokens=0)
def _full_trace(**overrides: object) -> QueryLabTrace:
from ragcore.web.query_lab.models import EffectiveFilterTrace
values: dict[str, object] = {
"retrieval_run_id": uuid4(),
"query": "example query",
"profile": "hybrid-v1",
"effective_filters": EffectiveFilterTrace(
requested_space_ids=(),
effective_space_ids=(),
denied_space_ids=(),
trust_required_space_ids=(),
requested_content_types=(),
applied_content_types=(),
requested_languages=(),
applied_languages=(),
unsupported_filter_fields=(),
),
"dense_candidates": (),
"sparse_candidates": (),
"fusion": FusionTrace(candidates=(), suppressed_count=0),
"rerank": RerankTrace(
state="skipped", degradation_code=None, candidates=(), elapsed_ms=0.0
),
"parent_expansion": ParentExpansionTrace(0, 0, 0),
"context": ContextTrace(
token_count=0, max_context_tokens=100, block_count=0, citation_ids=()
),
"answerability": AnswerabilityTrace(
status="not_answerable",
reasons=("insufficient_evidence",),
evidence_count=0,
document_diversity=0,
source_independence=0,
facet_coverage_ratio=0.0,
thresholds_version="v1",
),
"citations": (),
"timings_ms": {},
"revisions": RevisionTrace(None, None, "technical/v1", None, "v1"),
"degraded": False,
"degradation_codes": (),
}
values.update(overrides)
return QueryLabTrace(**values)
def test_trace_rejects_unsafe_timing_name() -> None:
with pytest.raises(ValueError):
_full_trace(timings_ms={"not a safe name": 1.0})
def test_trace_rejects_negative_timing() -> None:
with pytest.raises(ValueError):
_full_trace(timings_ms={"embed_query": -1.0})
def test_trace_rejects_duplicate_degradation_codes() -> None:
with pytest.raises(ValueError):
_full_trace(degradation_codes=("backend_timeout", "backend_timeout"))
def test_trace_rejects_too_many_citations() -> None:
from ragcore.web.query_lab.models import CitationTrace
citations = tuple(
CitationTrace(
id=uuid4(),
document_id=uuid4(),
chunk_id=uuid4(),
excerpt_preview="text",
excerpt_truncated=False,
location={},
source_uri="ragcore://source/x",
)
for _ in range(21)
)
with pytest.raises(ValueError):
_full_trace(citations=citations)
def test_trace_accepts_a_well_formed_minimal_result() -> None:
trace = _full_trace()
assert trace.degraded is False
@@ -0,0 +1,203 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from ragcore.api.v1.search.service import AuthorizationInputs
from ragcore.application.authorization import AuthorizationService
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
from ragcore.domain.authorization import PermissionDenied, Principal, PrincipalType
from ragcore.domain.control import DataPolicy, KnowledgeSpace, Role, UserGrant
from ragcore.web.query_lab.executor import QueryLabExecutionRequest
from ragcore.web.query_lab.models import (
AnswerabilityTrace,
ContextTrace,
EffectiveFilterTrace,
FusionTrace,
ParentExpansionTrace,
QueryLabQuery,
QueryLabTrace,
RerankTrace,
RevisionTrace,
)
from ragcore.web.query_lab.service import QueryLabService
WORKSPACE_ID = uuid4()
SPACE_ID = uuid4()
def _space() -> KnowledgeSpace:
return KnowledgeSpace(
id=SPACE_ID,
workspace_id=WORKSPACE_ID,
name="Docs",
slug="docs",
active=True,
trust_required=False,
data_policy=DataPolicy.LOCAL_ONLY,
retention_days=None,
)
def _user_grant(user_id, role: Role = Role.VIEWER) -> UserGrant:
return UserGrant(
id=uuid4(),
workspace_id=WORKSPACE_ID,
user_id=user_id,
knowledge_space_id=SPACE_ID,
role=role,
active=True,
)
class StaticInputsProvider:
def __init__(self, spaces=(), user_grants=()) -> None:
self._spaces = spaces
self._user_grants = user_grants
async def load(self, principal: Principal) -> AuthorizationInputs:
return AuthorizationInputs(
spaces=self._spaces, grants=(), user_grants=self._user_grants, application=None
)
class RecordingExecutor:
def __init__(self) -> None:
self.requests: list[QueryLabExecutionRequest] = []
async def execute(self, request: QueryLabExecutionRequest) -> QueryLabTrace:
self.requests.append(request)
return _trace(request)
def _trace(request: QueryLabExecutionRequest) -> QueryLabTrace:
return QueryLabTrace(
retrieval_run_id=request.retrieval_run_id,
query=request.query.query,
profile=request.query.profile,
effective_filters=EffectiveFilterTrace((), (), (), (), (), (), (), (), ()),
dense_candidates=(),
sparse_candidates=(),
fusion=FusionTrace(candidates=(), suppressed_count=0),
rerank=RerankTrace(state="skipped", degradation_code=None, candidates=(), elapsed_ms=0.0),
parent_expansion=ParentExpansionTrace(0, 0, 0),
context=ContextTrace(token_count=0, max_context_tokens=100, block_count=0, citation_ids=()),
answerability=AnswerabilityTrace(
status="not_answerable",
reasons=("insufficient_evidence",),
evidence_count=0,
document_diversity=0,
source_independence=0,
facet_coverage_ratio=0.0,
thresholds_version="v1",
),
citations=(),
timings_ms={},
revisions=RevisionTrace(None, None, "technical/v1", None, "v1"),
degraded=False,
degradation_codes=(),
)
def _service(executor: RecordingExecutor, spaces=(), user_grants=()) -> QueryLabService:
return QueryLabService(
authorization=AuthorizationService(),
retrieval_authorization=RetrievalAuthorizationService(),
inputs=StaticInputsProvider(spaces, user_grants),
executor=executor,
)
def _principal(role: Role) -> Principal:
return Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset({role}))
def test_can_use_reflects_the_privilege_gate_without_side_effects() -> None:
service = _service(RecordingExecutor())
assert service.can_use(_principal(Role.SPACE_MANAGER)) is True
assert service.can_use(_principal(Role.PLATFORM_ADMIN)) is True
assert service.can_use(_principal(Role.VIEWER)) is False
assert service.can_use(_principal(Role.EDITOR)) is False
@pytest.mark.asyncio
async def test_viewer_is_denied_query_lab_even_with_a_valid_grant() -> None:
principal = _principal(Role.VIEWER)
executor = RecordingExecutor()
service = _service(executor, spaces=(_space(),), user_grants=(_user_grant(principal.id),))
with pytest.raises(PermissionDenied):
await service.run(principal, QueryLabQuery(query="test"))
assert executor.requests == []
@pytest.mark.asyncio
async def test_editor_is_denied_query_lab_even_with_a_valid_grant() -> None:
principal = _principal(Role.EDITOR)
executor = RecordingExecutor()
service = _service(executor, spaces=(_space(),), user_grants=(_user_grant(principal.id),))
with pytest.raises(PermissionDenied):
await service.run(principal, QueryLabQuery(query="test"))
assert executor.requests == []
@pytest.mark.asyncio
async def test_space_manager_with_a_grant_can_run_and_scope_is_resolved_server_side() -> None:
principal = _principal(Role.SPACE_MANAGER)
executor = RecordingExecutor()
service = _service(
executor,
spaces=(_space(),),
user_grants=(_user_grant(principal.id, role=Role.SPACE_MANAGER),),
)
trace = await service.run(
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
)
assert trace.query == "test"
assert executor.requests[0].scope.effective_space_ids == frozenset({SPACE_ID})
@pytest.mark.asyncio
async def test_space_manager_without_any_grant_resolves_to_no_effective_spaces() -> None:
principal = _principal(Role.SPACE_MANAGER)
executor = RecordingExecutor()
service = _service(executor, spaces=(_space(),), user_grants=())
trace = await service.run(
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
)
assert executor.requests == []
assert trace.effective_filters.effective_space_ids == ()
@pytest.mark.asyncio
async def test_manual_unauthorized_requested_space_is_never_included() -> None:
principal = _principal(Role.SPACE_MANAGER)
executor = RecordingExecutor()
service = _service(
executor,
spaces=(_space(),),
user_grants=(_user_grant(principal.id, role=Role.SPACE_MANAGER),),
)
unauthorized_space = uuid4()
trace = await service.run(
principal, QueryLabQuery(query="test", requested_space_ids=(unauthorized_space,))
)
# The requested space was never granted, so the resolved scope is empty
# and the executor is never invoked -- a client cannot widen access by
# simply asking for a different space id.
assert executor.requests == []
assert trace.effective_filters.effective_space_ids == ()
@pytest.mark.asyncio
async def test_platform_admin_with_a_grant_can_run_query_lab() -> None:
principal = _principal(Role.PLATFORM_ADMIN)
executor = RecordingExecutor()
service = _service(
executor, spaces=(_space(),), user_grants=(_user_grant(principal.id, role=Role.VIEWER),)
)
trace = await service.run(
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
)
assert executor.requests[0].scope.effective_space_ids == frozenset({SPACE_ID})
assert trace.retrieval_run_id == executor.requests[0].retrieval_run_id