From 531607a1a1151676ad58ded53bedec8a693a0156 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 22:53:43 +0200 Subject: [PATCH 1/4] fix(tests): repair multi-user-basic Astrolabe integration suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Astrolabe PHP→Vue settings refactor dropped three stable element ids (#mcp-enable-background-button, #mcp-revoke-background-button, #mcp-revoke-background-form) that the multi-user-basic integration suite drives the background-sync enable/disable/revoke flows through. Their absence timed out the 5s Playwright locators and failed four tests: - test_astrolabe_multi_user_background_sync::test_multi_user_astrolabe_background_sync_enablement - test_astrolabe_multi_user_background_sync::test_revoke_background_sync_access - test_astrolabe_chunk_context::test_chunk_context_endpoint_uses_app_password - test_astrolabe_plotly_visualization::test_astrolabe_plotly_visualization_with_basic_auth (the latter two enable background sync via complete_astrolabe_authorization before exercising the app-password / indexed-search paths). Two-part fix: 1. Bump the astrolabe submodule to v0.20.1 (cbcoutinho/astrolabe#116), which restores the three element ids on the refactored NcButtons. 2. Defense-in-depth in the test helpers: resolve the enable/revoke buttons by their stable id first, falling back to the button's accessible name so a future id rename degrades to a slower-but-working lookup instead of a hard timeout. Avoids a combined `.or_()` locator, which would strict-mode-violate (the id button also matches by text). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...st_astrolabe_multi_user_background_sync.py | 82 ++++++++++++++----- third_party/astrolabe | 2 +- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/tests/integration/test_astrolabe_multi_user_background_sync.py b/tests/integration/test_astrolabe_multi_user_background_sync.py index eeb32c23..21477eb4 100644 --- a/tests/integration/test_astrolabe_multi_user_background_sync.py +++ b/tests/integration/test_astrolabe_multi_user_background_sync.py @@ -18,17 +18,59 @@ password minted + forwarded to MCP → background sync active → DB verificatio """ import logging +import re import subprocess import tempfile import anyio import pytest -from playwright.async_api import Page +from playwright.async_api import Locator, Page logger = logging.getLogger(__name__) pytestmark = [pytest.mark.integration, pytest.mark.multi_user_basic] +# Accessible-name fallbacks for the background-sync buttons. The stable element +# ids (#mcp-enable-background-button / #mcp-revoke-background-button) are the +# primary, refactor-proof hook restored in the Astrolabe frontend; resolving by +# button text as well means a future id rename degrades to a slower-but-working +# lookup instead of a hard 5s Playwright timeout. +_ENABLE_NAME = re.compile("Enable background indexing", re.IGNORECASE) +_REVOKE_NAME = re.compile("Disable background indexing", re.IGNORECASE) + + +async def _resolve_settings_button( + page: Page, button_id: str, name: re.Pattern, *, timeout: int = 5000 +) -> Locator: + """Resolve a background-sync settings button, preferring its stable id. + + Waits up to ``timeout`` for ``button_id`` to become visible; if it never + appears, falls back to the button's accessible name. Returning the id + locator unchanged when present keeps the match unambiguous (the id button + also matches by text, so a combined ``.or_()`` would strict-mode-violate). + """ + by_id = page.locator(button_id) + try: + await by_id.wait_for(timeout=timeout, state="visible") + return by_id + except Exception: + logger.warning( + "%s not found; falling back to accessible-name lookup", button_id + ) + by_name = page.get_by_role("button", name=name) + await by_name.wait_for(timeout=timeout, state="visible") + return by_name + + +async def _background_sync_enabled(page: Page) -> bool: + """True when the settings page is in the enabled state (revoke control shown). + + Checks the stable id first, then the accessible name as a fallback. + """ + if await page.locator("#mcp-revoke-background-button").count() > 0: + return True + return await page.get_by_role("button", name=_REVOKE_NAME).count() > 0 + async def login_to_nextcloud(page: Page, username: str, password: str): """Helper function to login to Nextcloud via Playwright. @@ -113,19 +155,20 @@ async def enable_background_sync(page: Page, username: str) -> bool: ) await anyio.sleep(1) - if await page.locator("#mcp-revoke-background-button").count() > 0: + if await _background_sync_enabled(page): logger.info("✓ Background indexing already enabled for %s", username) return True - enable_button = page.locator("#mcp-enable-background-button") - await enable_button.wait_for(timeout=5000, state="visible") + enable_button = await _resolve_settings_button( + page, "#mcp-enable-background-button", _ENABLE_NAME + ) await enable_button.click() logger.info("Clicked 'Enable background indexing' for %s", username) # On success the page JS reloads to the enabled state (revoke form shown). try: - await page.locator("#mcp-revoke-background-button").wait_for( - timeout=15000, state="visible" + await _resolve_settings_button( + page, "#mcp-revoke-background-button", _REVOKE_NAME, timeout=15000 ) logger.info("✓ Background indexing enabled for %s", username) return True @@ -507,21 +550,19 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool: # Wait for page to load await anyio.sleep(1) - # The revoke form (#mcp-revoke-background-form) is only rendered while - # background indexing is enabled. - revoke_button = page.locator("#mcp-revoke-background-button") - try: - if await revoke_button.count() == 0: - logger.warning( - "Background indexing not enabled for %s, nothing to revoke", username - ) - return False - except Exception: - logger.warning("Could not find revoke button for %s", username) + # The revoke control (#mcp-revoke-background-button, inside the + # #mcp-revoke-background-form container) is only rendered while background + # indexing is enabled. + if not await _background_sync_enabled(page): + logger.warning( + "Background indexing not enabled for %s, nothing to revoke", username + ) return False try: - await revoke_button.wait_for(timeout=5000, state="visible") + revoke_button = await _resolve_settings_button( + page, "#mcp-revoke-background-button", _REVOKE_NAME + ) logger.info("Found 'Disable background indexing' button") except Exception: screenshot_path = ( @@ -586,9 +627,10 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool: pass # After revoke + reload the settings page returns to the un-provisioned - # state: the revoke button is gone and the app-password input is shown again. + # state: the revoke button is gone and the enable button is shown again. try: - if await page.locator("#mcp-revoke-background-button").is_visible(timeout=2000): + await anyio.sleep(2) + if await _background_sync_enabled(page): logger.error("Revoke button still visible for %s after revoke!", username) screenshot_path = ( f"{tempfile.gettempdir()}/astrolabe_revoke_still_enabled_{username}.png" diff --git a/third_party/astrolabe b/third_party/astrolabe index 0d9edc64..af55853a 160000 --- a/third_party/astrolabe +++ b/third_party/astrolabe @@ -1 +1 @@ -Subproject commit 0d9edc647dc5eae04292e7e048c7590c93763d93 +Subproject commit af55853adfe1041dbae289a668f2e67e3ba2ed90 From 338fb78199a5c385a597cc6c6b3eddfc16ff6692 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 22:55:10 +0200 Subject: [PATCH 2/4] ci: add astrolabe submodule to docker-compose --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ed95783b..6dcb7a0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: # Mount OIDC development directory outside /var/www/html to avoid rsync conflicts # The post-installation hook will register /opt/apps as an additional app directory #- ./third_party:/opt/apps:ro - #- ./third_party/astrolabe:/opt/apps/astrolabe:ro + - ./third_party/astrolabe:/opt/apps/astrolabe:ro #- ./third_party/oidc:/opt/apps/oidc:ro environment: - NEXTCLOUD_TRUSTED_DOMAINS=app From 533bd79949aa2bf0d75b20cc9edbb4934965d51c Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 23:33:03 +0200 Subject: [PATCH 3/4] fix(tests): fast vector-sync cadence for multi-user-basic CI service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After restoring the background-sync UI ids, the two indexing-dependent multi-user-basic tests (chunk_context_uses_app_password, plotly_with_basic_auth) surfaced a second, previously-masked failure: they provision a user, create a note, then wait ~90s for it to be indexed. The mcp-multi-user-basic service ran with the production cadence — scan interval 60s and the default user-poll interval 60s. A freshly-provisioned user isn't even *discovered* by the background-sync user manager for up to 60s, leaving too little of the 90s budget for the scan + single-worker indexing to finish (observed: pending docs still "syncing" at timeout, or the scanner not yet started → "idle" with 0 indexed). Match the single-user service's short cadence (5s) and add a matching 5s user-poll interval so discovery + scan + index complete well within the test budget. Test-only config; production deployments set their own intervals. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6dcb7a0a..151d9296 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,7 +160,15 @@ services: - TOKEN_STORAGE_DB=/app/data/tokens.db - ENABLE_SEMANTIC_SEARCH=true - - VECTOR_SYNC_SCAN_INTERVAL=60 + # Fast discovery + scan cadence for integration tests: the multi-user + # background-sync suite provisions a user, creates a note, then waits + # ~90s for it to be indexed. With the production defaults (user-poll 60s + # + scan 60s) a freshly-provisioned user often isn't even discovered + # within that budget, so the wait times out (note never indexed). Match + # the single-user service's short cadence so discovery + scan + index + # complete promptly under CI load. + - VECTOR_SYNC_USER_POLL_INTERVAL=5 + - VECTOR_SYNC_SCAN_INTERVAL=5 - VECTOR_SYNC_PROCESSOR_WORKERS=1 # OAuth credentials for background sync (optional - uses DCR if not provided) From 042b9aa295dc32fa2aa33707daf0d82b0769f03a Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 4 Jun 2026 23:49:03 +0200 Subject: [PATCH 4/4] fix(tests): moderate re-scan interval to stop multi-user index churn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lowering VECTOR_SYNC_SCAN_INTERVAL to 5s (previous commit) fixed user discovery on nc31 but exposed re-scan churn on the slower nc32 runner: each scan re-queues the user's entire corpus, so a 5s cadence floods the single processor worker faster than it drains (pending climbed to 20-30+ while indexed stayed 0, status "syncing"). Discovery latency and re-scan churn are separate knobs. Keep USER_POLL_INTERVAL short (5s) for prompt discovery — the per-user scanner runs its initial scan immediately on start, so the corpus is queued once right away — but restore a moderate SCAN_INTERVAL (30s) so re-scans don't re-flood the queue. Indexing of the one-time initial scan completes well inside the test's 90s budget; a note created just after that scan is still picked up by the 30s re-scan. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 151d9296..1be09d8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,15 +160,21 @@ services: - TOKEN_STORAGE_DB=/app/data/tokens.db - ENABLE_SEMANTIC_SEARCH=true - # Fast discovery + scan cadence for integration tests: the multi-user - # background-sync suite provisions a user, creates a note, then waits - # ~90s for it to be indexed. With the production defaults (user-poll 60s - # + scan 60s) a freshly-provisioned user often isn't even discovered - # within that budget, so the wait times out (note never indexed). Match - # the single-user service's short cadence so discovery + scan + index - # complete promptly under CI load. + # Tuned cadence for the multi-user background-sync integration suite, + # which provisions a user, creates a note, then waits ~90s for it to be + # indexed. Two independent knobs matter here: + # * USER_POLL_INTERVAL gates how fast the background-sync user manager + # *discovers* a freshly-provisioned user (its scanner runs an initial + # scan immediately on start). The 60s default left too little of the + # 90s budget, so drop it to 5s for prompt discovery. + # * SCAN_INTERVAL gates *re-scan* churn. Each scan re-queues the user's + # whole corpus, so a very short interval (e.g. 5s) floods the single + # processor worker faster than it drains on slower CI runners + # (pending climbs, indexed stays 0). Keep it moderate: the immediate + # initial scan already indexes the corpus once; 30s re-scans avoid + # the flood while still catching a note created just after that scan. - VECTOR_SYNC_USER_POLL_INTERVAL=5 - - VECTOR_SYNC_SCAN_INTERVAL=5 + - VECTOR_SYNC_SCAN_INTERVAL=30 - VECTOR_SYNC_PROCESSOR_WORKERS=1 # OAuth credentials for background sync (optional - uses DCR if not provided)