test(points): cover soft delete, relinking, and the file sweep

Why:
- The failure modes worth testing are races, and they are cheap to force
  against the fake and expensive to observe anywhere else.

Changes:
- Unit tests for both boundaries, the repeat delete, a missing neighbour, the
  traversal property after several deletes, and — via a repository that bumps a
  rival's version before each apply — both the partial-apply repair and the
  unconvergent 409.
- One new shared contract scenario (a multi-point batch applies every patch,
  including nulling a pointer) so it runs against the fake and real Qdrant.
- HTTP tests against real Postgres and Qdrant for relinking, soft-not-hard
  delete, cross-tenant 404, and scope enforcement on both routes.
- create_source_file factory for tests addressing a file without uploading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 17:12:56 +03:30
parent b58f4630f3
commit 73bdac0da2
4 changed files with 732 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.application.auth.keys import generate_api_key, hash_secret
from src.infrastructure.postgres.models.api_key import ApiKey
from src.infrastructure.postgres.models.source_file import SourceFile
from src.infrastructure.postgres.models.tenant import Tenant
from src.infrastructure.postgres.models.tenant_domain import TenantDomain
@@ -50,6 +51,36 @@ async def create_api_key(
return api_key, full_key
async def create_source_file(
session: AsyncSession,
*,
tenant: Tenant,
source_file_id: uuid.UUID | None = None,
domain: str = "general",
status: str = "active",
) -> SourceFile:
"""A `source_files` row for tests that address a file without uploading one.
`DELETE /v1/files/{file_id}` authorizes against this row before touching a
single point, so a delete test needs it even though the interesting state
lives in Qdrant.
"""
source_file = SourceFile(
id=source_file_id or uuid.uuid4(),
tenant_id=tenant.id,
domain=domain,
source_filename="handbook.docx",
source_type="docx",
content_sha256="0" * 64,
byte_size=1024,
storage_uri="s3://bucket/key",
status=status,
)
session.add(source_file)
await session.flush()
return source_file
async def create_tenant_domain(
session: AsyncSession,
*,