From 21ce620a847542015e35c638e39718f89122e45c Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 16 Jun 2026 01:56:54 +0200 Subject: [PATCH] =?UTF-8?q?fix(vector-sync):=20address=20round-6=20review?= =?UTF-8?q?=20=E2=80=94=20rename=20shadowed=20var,=20add=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vector_sync route: rename the response dict from `body` to `resp` so it no longer shadows the request `body` (maintenance trap) - scanner: comment the intentional files-vs-text purge timing asymmetry - tests: add the all-text-types-disabled backstop case (empty allow-set) Co-Authored-By: Claude Opus 4.8 (1M context) --- nextcloud_mcp_server/api/vector_sync.py | 6 +++--- nextcloud_mcp_server/vector/scanner.py | 3 +++ .../vector/test_scanner_consent_backstop.py | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/nextcloud_mcp_server/api/vector_sync.py b/nextcloud_mcp_server/api/vector_sync.py index 000e2cd5..3944de33 100644 --- a/nextcloud_mcp_server/api/vector_sync.py +++ b/nextcloud_mcp_server/api/vector_sync.py @@ -121,16 +121,16 @@ async def purge_doc_types_route(request: Request) -> JSONResponse: # NOT purged (consent not yet enforced for them) — the scanner backstop # still catches these, but the caller shouldn't assume full success. failed = [dt for dt in dict.fromkeys(doc_types) if dt not in purged] - body: dict = {"purged": purged} + resp: dict = {"purged": purged} if failed: - body["failed"] = failed + resp["failed"] = failed logger.info( "Vector-sync purge by admin %s: purged=%s failed=%s", user_id, purged, failed, ) - return JSONResponse(body) + return JSONResponse(resp) except ProvisioningRequiredError as e: logger.info("Provisioning required for user %s: %s", user_id, e) diff --git a/nextcloud_mcp_server/vector/scanner.py b/nextcloud_mcp_server/vector/scanner.py index 907bb021..c1623e54 100644 --- a/nextcloud_mcp_server/vector/scanner.py +++ b/nextcloud_mcp_server/vector/scanner.py @@ -594,6 +594,9 @@ async def scan_user_documents( # indexed. The deletion-reconcile below then sees every indexed # file as "missing" and purges it after the grace period — the # backstop for the eager purge Astrolabe runs on disable. + # Asymmetry (intentional): files purge up to 1.5x scan_interval + # later than text types, which get immediate one-shot backstop + # deletes via _enqueue_deletes_for_disabled_types. logger.debug( "[SCAN-%s] Files disabled by admin for %s; skipping tagged-file discovery", scan_id, diff --git a/tests/unit/vector/test_scanner_consent_backstop.py b/tests/unit/vector/test_scanner_consent_backstop.py index 5b4cefd3..e87ebed9 100644 --- a/tests/unit/vector/test_scanner_consent_backstop.py +++ b/tests/unit/vector/test_scanner_consent_backstop.py @@ -73,6 +73,25 @@ async def test_enqueues_deletes_for_disabled_text_type(monkeypatch): assert all(t.operation == "delete" and t.doc_type == "note" for t in sent) +async def test_all_text_types_disabled_enqueues_all(monkeypatch): + # Admin disabled everything at once (empty allow-set): every text type's + # indexed points are enqueued for deletion in a single call. + _patch_qdrant( + monkeypatch, {"note": ["n1"], "news_item": ["ni1"], "deck_card": ["d1"]} + ) + sent: list = [] + stream = _producer(AsyncMock(side_effect=lambda t: sent.append(t))) + + queued = await _enqueue_deletes_for_disabled_types("alice", stream, frozenset(), 1) + + assert queued == 3 + assert {(t.doc_type, t.doc_id) for t in sent} == { + ("note", "n1"), + ("news_item", "ni1"), + ("deck_card", "d1"), + } + + async def test_noop_when_allowed_is_none(monkeypatch): # Fail-open: a transient capability read must never trigger deletion. send = AsyncMock()