Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,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)
|
||||
Reference in new issue
Block a user