Merge pull request #854 from cbcoutinho/fix/astrolabe-multi-user-integration-tests

fix(tests): repair multi-user-basic Astrolabe integration suite
This commit is contained in:
Chris Coutinho
2026-06-05 00:06:26 +02:00
committed by GitHub
3 changed files with 79 additions and 23 deletions
+16 -2
View File
@@ -36,7 +36,7 @@ services:
# Mount OIDC development directory outside /var/www/html to avoid rsync conflicts # 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 # The post-installation hook will register /opt/apps as an additional app directory
#- ./third_party:/opt/apps:ro #- ./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 #- ./third_party/oidc:/opt/apps/oidc:ro
environment: environment:
- NEXTCLOUD_TRUSTED_DOMAINS=app - NEXTCLOUD_TRUSTED_DOMAINS=app
@@ -160,7 +160,21 @@ services:
- TOKEN_STORAGE_DB=/app/data/tokens.db - TOKEN_STORAGE_DB=/app/data/tokens.db
- ENABLE_SEMANTIC_SEARCH=true - ENABLE_SEMANTIC_SEARCH=true
- VECTOR_SYNC_SCAN_INTERVAL=60 # 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=30
- VECTOR_SYNC_PROCESSOR_WORKERS=1 - VECTOR_SYNC_PROCESSOR_WORKERS=1
# OAuth credentials for background sync (optional - uses DCR if not provided) # OAuth credentials for background sync (optional - uses DCR if not provided)
@@ -18,17 +18,59 @@ password minted + forwarded to MCP → background sync active → DB verificatio
""" """
import logging import logging
import re
import subprocess import subprocess
import tempfile import tempfile
import anyio import anyio
import pytest import pytest
from playwright.async_api import Page from playwright.async_api import Locator, Page
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
pytestmark = [pytest.mark.integration, pytest.mark.multi_user_basic] 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): async def login_to_nextcloud(page: Page, username: str, password: str):
"""Helper function to login to Nextcloud via Playwright. """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) 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) logger.info("✓ Background indexing already enabled for %s", username)
return True return True
enable_button = page.locator("#mcp-enable-background-button") enable_button = await _resolve_settings_button(
await enable_button.wait_for(timeout=5000, state="visible") page, "#mcp-enable-background-button", _ENABLE_NAME
)
await enable_button.click() await enable_button.click()
logger.info("Clicked 'Enable background indexing' for %s", username) logger.info("Clicked 'Enable background indexing' for %s", username)
# On success the page JS reloads to the enabled state (revoke form shown). # On success the page JS reloads to the enabled state (revoke form shown).
try: try:
await page.locator("#mcp-revoke-background-button").wait_for( await _resolve_settings_button(
timeout=15000, state="visible" page, "#mcp-revoke-background-button", _REVOKE_NAME, timeout=15000
) )
logger.info("✓ Background indexing enabled for %s", username) logger.info("✓ Background indexing enabled for %s", username)
return True return True
@@ -507,21 +550,19 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
# Wait for page to load # Wait for page to load
await anyio.sleep(1) await anyio.sleep(1)
# The revoke form (#mcp-revoke-background-form) is only rendered while # The revoke control (#mcp-revoke-background-button, inside the
# background indexing is enabled. # #mcp-revoke-background-form container) is only rendered while background
revoke_button = page.locator("#mcp-revoke-background-button") # indexing is enabled.
try: if not await _background_sync_enabled(page):
if await revoke_button.count() == 0:
logger.warning( logger.warning(
"Background indexing not enabled for %s, nothing to revoke", username "Background indexing not enabled for %s, nothing to revoke", username
) )
return False return False
except Exception:
logger.warning("Could not find revoke button for %s", username)
return False
try: 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") logger.info("Found 'Disable background indexing' button")
except Exception: except Exception:
screenshot_path = ( screenshot_path = (
@@ -586,9 +627,10 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
pass pass
# After revoke + reload the settings page returns to the un-provisioned # 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: 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) logger.error("Revoke button still visible for %s after revoke!", username)
screenshot_path = ( screenshot_path = (
f"{tempfile.gettempdir()}/astrolabe_revoke_still_enabled_{username}.png" f"{tempfile.gettempdir()}/astrolabe_revoke_still_enabled_{username}.png"