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

@@ -329,6 +329,51 @@ async def scenario_patch_cannot_cross_tenants(
assert victim.content == "onboarding checklist for new staff"
async def scenario_patch_batch_applies_every_patch_in_one_call(
repository: PointRepository, *, tenant_a: uuid.UUID, file_a: uuid.UUID
) -> None:
"""One `apply_patches` call carrying several points applies all of them.
This is the primitive soft delete is built on: a deactivation plus its two
neighbour relinks go out together, and a batch that silently applied only
its first operation would leave the pointer chain broken — the exact defect
ADR-0002 rules out. Nulling a pointer is included because `None` and
"absent" are different payload values, and only one of them clears a link.
"""
deleted, previous, following = (point_id_for(file_a, index) for index in (1, 0, 2))
await repository.apply_patches(
tenant_id=tenant_a,
patches=[
PayloadPatch(
point_id=deleted,
payload={"is_active": False, "version": 2},
expected_version=1,
),
PayloadPatch(
point_id=previous,
payload={"next_chunk_id": str(following), "version": 2},
expected_version=1,
),
PayloadPatch(
point_id=following,
payload={"previous_chunk_id": None, "version": 2},
expected_version=1,
),
],
)
applied = {
point.point_id: point
for point in await repository.get_many(
tenant_id=tenant_a, point_ids=[deleted, previous, following]
)
}
assert applied[deleted].is_active is False
assert applied[previous].next_chunk_id == following
assert applied[following].previous_chunk_id is None
assert [applied[point_id].version for point_id in (deleted, previous, following)] == [2, 2, 2]
# Scenarios that only read, safe to run in any order against one seeded corpus.
READ_SCENARIOS = (
scenario_get_returns_point_within_tenant,
@@ -350,4 +395,5 @@ WRITE_SCENARIOS = (
scenario_patch_applies_when_version_matches,
scenario_patch_is_a_noop_when_version_is_stale,
scenario_patch_cannot_cross_tenants,
scenario_patch_batch_applies_every_patch_in_one_call,
)