refactor: convert f-string logging to lazy %-style format (G004)

Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.

Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.

Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
  (printf-style specs aren't 1:1 with Python format specs, so we
  delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved

Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-13 01:12:17 +02:00
co-authored by Claude Opus 4.7
parent a4e6125d28
commit 665cb9b1eb
112 changed files with 2534 additions and 1859 deletions
+7 -7
View File
@@ -79,7 +79,7 @@ async def create_generation_provider(provider_name: str) -> Provider:
embedding_model=None, # Generation only
generation_model=generation_model,
)
logger.info(f"Created OpenAI generation provider: model={generation_model}")
logger.info("Created OpenAI generation provider: model=%s", generation_model)
return provider
elif provider_name == "ollama":
@@ -96,7 +96,7 @@ async def create_generation_provider(provider_name: str) -> Provider:
embedding_model=None, # Generation only
generation_model=generation_model,
)
logger.info(f"Created Ollama generation provider: model={generation_model}")
logger.info("Created Ollama generation provider: model=%s", generation_model)
return provider
elif provider_name == "anthropic":
@@ -114,7 +114,7 @@ async def create_generation_provider(provider_name: str) -> Provider:
api_key=api_key,
generation_model=generation_model,
)
logger.info(f"Created Anthropic generation provider: model={generation_model}")
logger.info("Created Anthropic generation provider: model=%s", generation_model)
return provider
elif provider_name == "bedrock":
@@ -133,7 +133,7 @@ async def create_generation_provider(provider_name: str) -> Provider:
embedding_model=None, # Generation only
generation_model=generation_model,
)
logger.info(f"Created Bedrock generation provider: model={generation_model}")
logger.info("Created Bedrock generation provider: model=%s", generation_model)
return provider
else:
@@ -178,7 +178,7 @@ async def create_embedding_provider(provider_name: str) -> Provider:
embedding_model=embedding_model,
generation_model=None, # Embeddings only
)
logger.info(f"Created OpenAI embedding provider: model={embedding_model}")
logger.info("Created OpenAI embedding provider: model=%s", embedding_model)
return provider
elif provider_name == "ollama":
@@ -195,7 +195,7 @@ async def create_embedding_provider(provider_name: str) -> Provider:
embedding_model=embedding_model,
generation_model=None, # Embeddings only
)
logger.info(f"Created Ollama embedding provider: model={embedding_model}")
logger.info("Created Ollama embedding provider: model=%s", embedding_model)
return provider
elif provider_name == "bedrock":
@@ -214,7 +214,7 @@ async def create_embedding_provider(provider_name: str) -> Provider:
embedding_model=embedding_model,
generation_model=None, # Embeddings only
)
logger.info(f"Created Bedrock embedding provider: model={embedding_model}")
logger.info("Created Bedrock embedding provider: model=%s", embedding_model)
return provider
else:
+6 -4
View File
@@ -62,7 +62,7 @@ def create_sampling_callback(provider: Provider):
params: types.CreateMessageRequestParams,
) -> types.CreateMessageResult | types.ErrorData:
"""Handle sampling requests using the configured provider."""
logger.debug(f"Sampling callback invoked with {len(params.messages)} messages")
logger.debug("Sampling callback invoked with %s messages", len(params.messages))
# Extract messages and build prompt
messages_text = []
@@ -77,7 +77,7 @@ def create_sampling_callback(provider: Provider):
if params.systemPrompt:
prompt = f"System: {params.systemPrompt}\n\n{prompt}"
logger.debug(f"Generating response for prompt ({len(prompt)} chars)")
logger.debug("Generating response for prompt (%s chars)", len(prompt))
try:
# Generate response using provider
@@ -87,7 +87,9 @@ def create_sampling_callback(provider: Provider):
max_tokens=params.maxTokens,
)
logger.info(f"Sampling completed: {len(response)} chars from {model_name}")
logger.info(
"Sampling completed: %s chars from %s", len(response), model_name
)
return types.CreateMessageResult(
role="assistant",
@@ -96,7 +98,7 @@ def create_sampling_callback(provider: Provider):
stopReason="endTurn",
)
except Exception as e:
logger.error(f"Generation failed ({provider.__class__.__name__}): {e}")
logger.error("Generation failed (%s): %s", provider.__class__.__name__, e)
return types.ErrorData(
code=types.INTERNAL_ERROR,
message=f"Generation failed: {e!s}",
@@ -91,8 +91,10 @@ async def _poll_astrolabe_search_for_note(
)
if note_result is not None:
logger.info(
f"Note {note_id} surfaced in Astrolabe search after {attempts} "
f"attempts (~{attempts * 2}s)"
"Note %s surfaced in Astrolabe search after %s attempts (~%ss)",
note_id,
attempts,
attempts * 2,
)
return note_result
await anyio.sleep(2)
@@ -245,7 +247,7 @@ async def test_chunk_context_endpoint_uses_app_password(
"nc_notes_delete_note", {"note_id": note_id}
)
except Exception as cleanup_err:
logger.warning(f"Cleanup failed for note {note_id}: {cleanup_err}")
logger.warning("Cleanup failed for note %s: %s", note_id, cleanup_err)
await context.close()
@@ -40,7 +40,7 @@ async def login_to_nextcloud(page: Page, username: str, password: str):
"""
nextcloud_url = "http://localhost:8080"
logger.info(f"Logging in to Nextcloud as {username}...")
logger.info("Logging in to Nextcloud as %s...", username)
await page.goto(f"{nextcloud_url}/login", wait_until="networkidle")
# Fill in login form
@@ -68,7 +68,7 @@ async def login_to_nextcloud(page: Page, username: str, password: str):
assert "/login" not in current_url, (
f"Login failed for {username}, still on login page"
)
logger.info(f"✓ Successfully logged in as {username}")
logger.info("✓ Successfully logged in as %s", username)
async def navigate_to_astrolabe_settings(page: Page):
@@ -80,7 +80,7 @@ async def navigate_to_astrolabe_settings(page: Page):
nextcloud_url = "http://localhost:8080"
settings_url = f"{nextcloud_url}/settings/user/astrolabe"
logger.info(f"Navigating to Astrolabe settings: {settings_url}")
logger.info("Navigating to Astrolabe settings: %s", settings_url)
await page.goto(settings_url, wait_until="networkidle", timeout=30000)
# Verify we're on the settings page
@@ -110,7 +110,7 @@ async def authorize_search_access(page: Page, username: str) -> bool:
"""
nextcloud_url = "http://localhost:8080"
logger.info(f"Authorizing search access (Step 1) for {username}...")
logger.info("Authorizing search access (Step 1) for %s...", username)
# Check if already on Astrolabe settings page, if not navigate there
if "/settings/user/astrolabe" not in page.url:
@@ -124,7 +124,7 @@ async def authorize_search_access(page: Page, username: str) -> bool:
# Check for "Active" badge (fully configured state)
active_badge = page.get_by_text("Active", exact=True)
if await active_badge.count() > 0 and await active_badge.is_visible():
logger.info(f"✓ Already fully authorized for {username} (Active badge)")
logger.info("✓ Already fully authorized for %s (Active badge)", username)
return True
except Exception:
pass
@@ -136,7 +136,7 @@ async def authorize_search_access(page: Page, username: str) -> bool:
step1_parent = step1_section.locator("..")
complete_badge = step1_parent.get_by_text("Complete", exact=True)
if await complete_badge.count() > 0 and await complete_badge.is_visible():
logger.info(f"✓ Step 1 already complete for {username}")
logger.info("✓ Step 1 already complete for %s", username)
return True
except Exception:
pass
@@ -146,36 +146,38 @@ async def authorize_search_access(page: Page, username: str) -> bool:
try:
await authorize_button.wait_for(timeout=5000, state="visible")
logger.info(f"Found Authorize button for {username}")
logger.info("Found Authorize button for %s", username)
except Exception:
# Take screenshot for debugging
screenshot_path = f"/tmp/astrolabe_no_authorize_button_{username}.png"
await page.screenshot(path=screenshot_path)
logger.error(
f"Could not find Authorize button for {username}. Screenshot: {screenshot_path}"
"Could not find Authorize button for %s. Screenshot: %s",
username,
screenshot_path,
)
raise ValueError(f"Authorize button not found for {username}")
# Click the Authorize button - this will redirect to OAuth provider
# Use force=True to bypass stability check which can timeout due to CSS transitions
await authorize_button.click(force=True)
logger.info(f"Clicked Authorize button for {username}")
logger.info("Clicked Authorize button for %s", username)
# Wait for OAuth redirect to complete
await page.wait_for_load_state("networkidle", timeout=30000)
logger.info(f"After networkidle, current URL: {page.url}")
logger.info("After networkidle, current URL: %s", page.url)
# Take screenshot to see current state
await page.screenshot(path=f"/tmp/astrolabe_after_authorize_{username}.png")
logger.info(f"Screenshot saved: /tmp/astrolabe_after_authorize_{username}.png")
logger.info("Screenshot saved: /tmp/astrolabe_after_authorize_%s.png", username)
# Handle OIDC consent screen if present
consent_handled = await _handle_oauth_consent_screen(page, username)
if consent_handled:
logger.info(f"✓ OAuth consent granted for {username}")
logger.info("✓ OAuth consent granted for %s", username)
else:
logger.info(
f"No consent screen required for {username} (may be previously authorized)"
"No consent screen required for %s (may be previously authorized)", username
)
# Wait for redirect back to Astrolabe settings
@@ -184,12 +186,12 @@ async def authorize_search_access(page: Page, username: str) -> bool:
await page.wait_for_url(
f"**{nextcloud_url}/settings/user/astrolabe**", timeout=30000
)
logger.info(f"Redirected back to Astrolabe settings for {username}")
logger.info("Redirected back to Astrolabe settings for %s", username)
except Exception:
# Check if we're already on settings page
if "/settings/user/astrolabe" not in page.url:
logger.warning(
f"Not redirected to Astrolabe settings, current URL: {page.url}"
"Not redirected to Astrolabe settings, current URL: %s", page.url
)
# Navigate manually
await page.goto(
@@ -205,7 +207,9 @@ async def authorize_search_access(page: Page, username: str) -> bool:
# First check if "Active" badge is shown (fully configured state)
active_badge = page.get_by_text("Active", exact=True)
if await active_badge.count() > 0 and await active_badge.is_visible():
logger.info(f"✓ OAuth authorization complete for {username} (Active badge)")
logger.info(
"✓ OAuth authorization complete for %s (Active badge)", username
)
return True
except Exception:
pass
@@ -217,7 +221,7 @@ async def authorize_search_access(page: Page, username: str) -> bool:
step1_parent = step1_section.locator("..")
complete_badge = step1_parent.get_by_text("Complete", exact=True)
await complete_badge.wait_for(timeout=5000, state="visible")
logger.info(f"✓ Step 1 OAuth authorization complete for {username}")
logger.info("✓ Step 1 OAuth authorization complete for %s", username)
return True
except Exception:
pass
@@ -226,7 +230,9 @@ async def authorize_search_access(page: Page, username: str) -> bool:
screenshot_path = f"/tmp/astrolabe_step1_not_complete_{username}.png"
await page.screenshot(path=screenshot_path)
logger.error(
f"Authorization badge not visible for {username}. Screenshot: {screenshot_path}"
"Authorization badge not visible for %s. Screenshot: %s",
username,
screenshot_path,
)
raise ValueError(f"OAuth authorization did not complete for {username}")
@@ -244,28 +250,28 @@ async def _handle_oauth_consent_screen(page: Page, username: str) -> bool:
True if consent was handled, False if no consent screen was found
"""
try:
logger.info(f"Checking for consent screen at URL: {page.url}")
logger.info("Checking for consent screen at URL: %s", page.url)
# Check if consent screen is present - try multiple selectors
# The consent screen may be #oidc-consent or use a different format
consent_div = await page.query_selector("#oidc-consent")
if consent_div:
logger.info(f"Consent screen detected via #oidc-consent for {username}")
logger.info("Consent screen detected via #oidc-consent for %s", username)
# Get consent screen data attributes for logging
client_name = await consent_div.get_attribute("data-client-name")
scopes_attr = await consent_div.get_attribute("data-scopes")
logger.info(f" Client: {client_name}")
logger.info(f" Requested scopes: {scopes_attr}")
logger.info(" Client: %s", client_name)
logger.info(" Requested scopes: %s", scopes_attr)
else:
# Check for Allow button directly (different consent screen format)
allow_button = page.locator('button:has-text("Allow")')
if await allow_button.count() > 0:
logger.info(f"Consent screen detected via Allow button for {username}")
logger.info("Consent screen detected via Allow button for %s", username)
else:
logger.info(f"No consent screen found for {username} at {page.url}")
logger.info("No consent screen found for %s at %s", username, page.url)
await page.screenshot(path=f"/tmp/no_consent_screen_{username}.png")
logger.info(f"Screenshot: /tmp/no_consent_screen_{username}.png")
logger.info("Screenshot: /tmp/no_consent_screen_%s.png", username)
return False
# Wait for Vue.js to render the Allow button
@@ -275,19 +281,19 @@ async def _handle_oauth_consent_screen(page: Page, username: str) -> bool:
except Exception as e:
screenshot_path = f"/tmp/consent_no_allow_button_{username}.png"
await page.screenshot(path=screenshot_path)
logger.error(f" Timeout waiting for Allow button: {e}")
logger.error(" Timeout waiting for Allow button: %s", e)
raise
# Check all scope checkboxes
scope_checkboxes = await page.query_selector_all('input[type="checkbox"]')
if scope_checkboxes:
logger.info(f" Found {len(scope_checkboxes)} scope checkboxes")
logger.info(" Found %s scope checkboxes", len(scope_checkboxes))
for i, checkbox in enumerate(scope_checkboxes):
is_checked = await checkbox.is_checked()
is_disabled = await checkbox.is_disabled()
if not is_checked and not is_disabled:
await checkbox.check()
logger.info(f" ✓ Checked scope checkbox {i + 1}")
logger.info(" ✓ Checked scope checkbox %s", i + 1)
# Click the Allow button using JavaScript (handles viewport issues)
allow_button_locator = page.locator('button:has-text("Allow")')
@@ -295,16 +301,16 @@ async def _handle_oauth_consent_screen(page: Page, username: str) -> bool:
# Debug: take screenshot before clicking Allow
await page.screenshot(path=f"/tmp/consent_before_allow_{username}.png")
logger.info(
f" Screenshot before Allow: /tmp/consent_before_allow_{username}.png"
" Screenshot before Allow: /tmp/consent_before_allow_%s.png", username
)
button_count = await allow_button_locator.count()
logger.info(f" Found {button_count} Allow button(s)")
logger.info(" Found %s Allow button(s)", button_count)
if button_count > 0:
current_url = page.url
logger.info(f" Current URL: {current_url}")
logger.info(f" Clicking Allow button for {username}...")
logger.info(" Current URL: %s", current_url)
logger.info(" Clicking Allow button for %s...", username)
# Use JavaScript click to handle consent buttons (proven pattern from conftest.py)
# This is more reliable than Playwright's click for Vue.js rendered buttons
@@ -327,10 +333,10 @@ async def _handle_oauth_consent_screen(page: Page, username: str) -> bool:
lambda url: url != current_url,
timeout=30000,
)
logger.info(f" URL changed to: {page.url}")
logger.info(" URL changed to: %s", page.url)
except Exception as wait_error:
# If URL didn't change, check console for errors
logger.warning(f" URL didn't change after click: {wait_error}")
logger.warning(" URL didn't change after click: %s", wait_error)
await page.screenshot(path=f"/tmp/consent_after_allow_{username}.png")
# Try alternative: manually POST consent and navigate
@@ -356,21 +362,21 @@ async def _handle_oauth_consent_screen(page: Page, username: str) -> bool:
}
"""
)
logger.info(f" Manual consent returned URL: {redirect_url}")
logger.info(" Manual consent returned URL: %s", redirect_url)
await page.goto(redirect_url, wait_until="networkidle")
except Exception as manual_error:
logger.error(f" Manual consent also failed: {manual_error}")
logger.error(" Manual consent also failed: %s", manual_error)
raise
await page.screenshot(path=f"/tmp/consent_after_allow_{username}.png")
logger.info(f" Consent granted for {username}")
logger.info(" Consent granted for %s", username)
return True
else:
logger.error(f" Allow button not found for {username}")
logger.error(" Allow button not found for %s", username)
return False
except Exception as e:
logger.error(f"Error handling consent screen for {username}: {e}")
logger.error("Error handling consent screen for %s: %s", username, e)
raise
@@ -387,7 +393,7 @@ async def generate_app_password(
Returns:
The generated app password string
"""
logger.info(f"Generating app password for {username}...")
logger.info("Generating app password for %s...", username)
nextcloud_url = "http://localhost:8080"
@@ -398,7 +404,7 @@ async def generate_app_password(
# Fill the app password input field (selector confirmed via Playwright MCP)
app_password_input = page.locator('input[placeholder="App name"]')
await app_password_input.fill(app_name)
logger.info(f"Entered app name: {app_name}")
logger.info("Entered app name: %s", app_name)
# Wait for Vue.js to react and enable the button (needs 1 second, not 0.5)
await anyio.sleep(1.0)
@@ -427,7 +433,7 @@ async def generate_app_password(
# Debug screenshot after clicking create
await page.screenshot(path=f"/tmp/app_password_after_create_{username}.png")
logger.info(
f"Screenshot after create: /tmp/app_password_after_create_{username}.png"
"Screenshot after create: /tmp/app_password_after_create_%s.png", username
)
# Find the Login input field which should have the username value
@@ -440,7 +446,7 @@ async def generate_app_password(
# Get all visible input elements
all_inputs = await page.locator('input[type="text"]').all()
logger.info(f"Found {len(all_inputs)} text input elements")
logger.info("Found %s text input elements", len(all_inputs))
# Check each input to find the one with the app password
for idx, input_elem in enumerate(all_inputs):
@@ -449,15 +455,18 @@ async def generate_app_password(
if value and "-" in value and len(value) > 20:
app_password = value.strip()
logger.info(
f"Found app password in input {idx}: '{app_password}' (length: {len(app_password)})"
"Found app password in input %s: '%s' (length: %s)",
idx,
app_password,
len(app_password),
)
break
except Exception as e:
logger.debug(f"Could not get value from input {idx}: {e}")
logger.debug("Could not get value from input %s: %s", idx, e)
continue
except Exception as e:
logger.error(f"Failed to find app password dialog or extract password: {e}")
logger.error("Failed to find app password dialog or extract password: %s", e)
if not app_password:
# Take screenshot for debugging
@@ -474,9 +483,9 @@ async def generate_app_password(
app_password,
):
logger.error(
f"Extracted password does not match expected format: '{app_password}'"
"Extracted password does not match expected format: '%s'", app_password
)
logger.error(f"Password repr: {repr(app_password)}")
logger.error("Password repr: %s", repr(app_password))
screenshot_path = f"/tmp/app_password_invalid_format_{username}.png"
await page.screenshot(path=screenshot_path)
raise ValueError(
@@ -484,7 +493,9 @@ async def generate_app_password(
)
logger.info(
f"✓ Generated app password for {username}: {app_password[:10]}... (validated)"
"✓ Generated app password for %s: %s... (validated)",
username,
app_password[:10],
)
# Close dialog with Escape key (bypasses CSS layout issues with h2 intercepting clicks)
@@ -509,7 +520,7 @@ async def enable_background_sync_via_app_password(
Returns:
True if background sync was enabled successfully
"""
logger.info(f"Enabling background sync via app password for {username}...")
logger.info("Enabling background sync via app password for %s...", username)
nextcloud_url = "http://localhost:8080"
@@ -524,7 +535,7 @@ async def enable_background_sync_via_app_password(
def log_response(resp):
response_info = f"{resp.status} {resp.url}"
network_responses.append(response_info)
logger.info(f"Response: {response_info}")
logger.info("Response: %s", response_info)
def log_console(msg):
console_messages.append(f"[{msg.type}] {msg.text}")
@@ -546,7 +557,7 @@ async def enable_background_sync_via_app_password(
# First check for overall "Active" badge (both steps complete)
active_text = page.get_by_text("Active", exact=True)
if await active_text.is_visible(timeout=2000):
logger.info(f"✓ Background sync already active for {username}")
logger.info("✓ Background sync already active for %s", username)
return True
except Exception:
pass
@@ -558,7 +569,7 @@ async def enable_background_sync_via_app_password(
step2_parent = step2_section.locator("..")
complete_badge = step2_parent.get_by_text("Complete", exact=True)
if await complete_badge.count() > 0 and await complete_badge.is_visible():
logger.info(f"✓ Step 2 (app password) already complete for {username}")
logger.info("✓ Step 2 (app password) already complete for %s", username)
return True
except Exception:
pass
@@ -580,7 +591,7 @@ async def enable_background_sync_via_app_password(
# Enter the app password
await app_password_input.fill(app_password)
logger.info(f"Entered app password for {username}")
logger.info("Entered app password for %s", username)
# Wait a moment for any validation to complete
await anyio.sleep(0.5)
@@ -588,14 +599,14 @@ async def enable_background_sync_via_app_password(
# Take screenshot before clicking Save to check for warnings
screenshot_path = f"/tmp/before_save_{username}.png"
await page.screenshot(path=screenshot_path)
logger.info(f"Screenshot taken before Save: {screenshot_path}")
logger.info("Screenshot taken before Save: %s", screenshot_path)
# Find and click the Save button
save_button = page.get_by_role("button", name="Save")
# Check if Save button is enabled
is_disabled = await save_button.is_disabled()
logger.info(f"Save button disabled state: {is_disabled}")
logger.info("Save button disabled state: %s", is_disabled)
await save_button.click()
logger.info("Clicked Save button")
@@ -604,24 +615,24 @@ async def enable_background_sync_via_app_password(
await anyio.sleep(0.5)
# Log network requests after clicking Save
logger.info(f"Network requests after Save for {username}:")
logger.info("Network requests after Save for %s:", username)
for req in network_requests[-10:]: # Last 10 requests
logger.info(f" {req}")
logger.info(" %s", req)
# Log network responses after clicking Save
logger.info(f"Network responses after Save for {username}:")
logger.info("Network responses after Save for %s:", username)
for resp in network_responses[-10:]: # Last 10 responses
logger.info(f" {resp}")
logger.info(" %s", resp)
# Check specifically for the credentials POST response
credentials_responses = [
r for r in network_responses if "background-sync/credentials" in r
]
if credentials_responses:
logger.info(f"Credentials endpoint response: {credentials_responses[-1]}")
logger.info("Credentials endpoint response: %s", credentials_responses[-1])
if "200" not in credentials_responses[-1]:
logger.error(
f"Credentials POST did not return 200 OK: {credentials_responses[-1]}"
"Credentials POST did not return 200 OK: %s", credentials_responses[-1]
)
else:
logger.warning("No response found for credentials endpoint!")
@@ -633,16 +644,16 @@ async def enable_background_sync_via_app_password(
# Log any console messages
if console_messages:
logger.info(f"Console messages for {username}:")
logger.info("Console messages for %s:", username)
for msg in console_messages:
logger.info(f" {msg}")
logger.info(" %s", msg)
# Check for error notifications (toast messages)
try:
error_toast = page.locator(".toastify.toast-error, .toast-error")
if await error_toast.count() > 0:
error_text = await error_toast.first.text_content()
logger.error(f"Error notification for {username}: {error_text}")
logger.error("Error notification for %s: %s", username, error_text)
except Exception:
pass
@@ -653,7 +664,7 @@ async def enable_background_sync_via_app_password(
if await active_text.count() > 0:
await active_text.wait_for(timeout=5000, state="visible")
logger.info(
f"✓ Background sync enabled for {username} - Active badge visible"
"✓ Background sync enabled for %s - Active badge visible", username
)
return True
except Exception:
@@ -667,7 +678,8 @@ async def enable_background_sync_via_app_password(
complete_badge = step2_parent.get_by_text("Complete", exact=True)
await complete_badge.wait_for(timeout=5000, state="visible")
logger.info(
f"✓ Step 2 (app password) enabled for {username} - Complete badge visible"
"✓ Step 2 (app password) enabled for %s - Complete badge visible",
username,
)
return True
except Exception:
@@ -677,8 +689,9 @@ async def enable_background_sync_via_app_password(
screenshot_path = f"/tmp/astrolabe_after_password_{username}.png"
await page.screenshot(path=screenshot_path)
logger.error(
f"Neither Active nor Complete badge appeared for {username}. "
f"Screenshot: {screenshot_path}"
"Neither Active nor Complete badge appeared for %s. Screenshot: %s",
username,
screenshot_path,
)
raise ValueError(f"Background sync setup did not complete for {username}")
@@ -702,7 +715,7 @@ async def complete_astrolabe_authorization(
Returns:
Dict with {"step1": bool, "step2": bool, "app_password": str | None}
"""
logger.info(f"Starting full Astrolabe authorization for {username}...")
logger.info("Starting full Astrolabe authorization for %s...", username)
result = {"step1": False, "step2": False, "app_password": None}
@@ -712,9 +725,9 @@ async def complete_astrolabe_authorization(
# Step 1: OAuth authorization
try:
result["step1"] = await authorize_search_access(page, username)
logger.info(f"✓ Step 1 complete for {username}")
logger.info("✓ Step 1 complete for %s", username)
except Exception as e:
logger.error(f"Step 1 failed for {username}: {e}")
logger.error("Step 1 failed for %s: %s", username, e)
raise
# Navigate back to settings if needed (OAuth might have redirected elsewhere)
@@ -728,7 +741,7 @@ async def complete_astrolabe_authorization(
step2_parent = step2_section.locator("..")
complete_badge = step2_parent.get_by_text("Complete", exact=True)
if await complete_badge.count() > 0 and await complete_badge.is_visible():
logger.info(f"✓ Step 2 already complete for {username}")
logger.info("✓ Step 2 already complete for %s", username)
result["step2"] = True
return result
except Exception:
@@ -738,7 +751,7 @@ async def complete_astrolabe_authorization(
try:
active_text = page.get_by_text("Active", exact=True)
if await active_text.count() > 0 and await active_text.is_visible():
logger.info(f"✓ Authorization already fully active for {username}")
logger.info("✓ Authorization already fully active for %s", username)
result["step2"] = True
return result
except Exception:
@@ -752,12 +765,12 @@ async def complete_astrolabe_authorization(
result["step2"] = await enable_background_sync_via_app_password(
page, username, app_password
)
logger.info(f"✓ Step 2 complete for {username}")
logger.info("✓ Step 2 complete for %s", username)
except Exception as e:
logger.error(f"Step 2 failed for {username}: {e}")
logger.error("Step 2 failed for %s: %s", username, e)
raise
logger.info(f"✓ Full Astrolabe authorization complete for {username}")
logger.info("✓ Full Astrolabe authorization complete for %s", username)
return result
@@ -773,7 +786,7 @@ async def verify_app_password_created(username: str) -> bool:
Returns:
True if background sync app password exists
"""
logger.info(f"Verifying background sync app password for {username}...")
logger.info("Verifying background sync app password for %s...", username)
# Query the database to check for background sync credentials
# Astrolabe stores app passwords in oc_preferences, not oc_authtoken
@@ -809,7 +822,7 @@ async def verify_app_password_created(username: str) -> bool:
)
output = result.stdout
logger.debug(f"Background sync credentials query result:\n{output}")
logger.debug("Background sync credentials query result:\\n%s", output)
# Check if background sync credentials exist
# We should see 3 rows: background_sync_password, background_sync_type, background_sync_provisioned_at
@@ -818,19 +831,22 @@ async def verify_app_password_created(username: str) -> bool:
if len(lines) >= 3: # Header + at least 2 data rows (password + type)
# Verify background_sync_type is "app_password"
if "app_password" in output:
logger.info(f"✓ Background sync app password stored for {username}")
logger.info("✓ Background sync app password stored for %s", username)
return True
else:
logger.warning(
f"Background sync credentials found but type is not app_password for {username}"
"Background sync credentials found but type is not app_password for %s",
username,
)
return False
else:
logger.warning(f"No background sync credentials found for {username}")
logger.warning("No background sync credentials found for %s", username)
return False
except Exception as e:
logger.error(f"Error checking background sync credentials for {username}: {e}")
logger.error(
"Error checking background sync credentials for %s: %s", username, e
)
return False
@@ -892,10 +908,13 @@ def clear_stale_test_state(clear_preferences: bool = False) -> None:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
logger.warning(
f"Failed to clear {label} (rc={result.returncode}): {result.stderr}"
"Failed to clear %s (rc=%s): %s",
label,
result.returncode,
result.stderr,
)
else:
logger.debug(f"Cleared {label}")
logger.debug("Cleared %s", label)
@pytest.mark.integration
@@ -946,7 +965,9 @@ async def test_multi_user_astrolabe_background_sync_enablement(
# Use nc_client to check if user exists
user_details = await nc_client.users.get_user_details(username)
logger.info(
f"✓ Confirmed {username} exists (display name: {user_details.displayname})"
"✓ Confirmed %s exists (display name: %s)",
username,
user_details.displayname,
)
except Exception as e:
raise AssertionError(
@@ -957,9 +978,9 @@ async def test_multi_user_astrolabe_background_sync_enablement(
results = {}
for username in test_users:
logger.info(f"\n{'=' * 60}")
logger.info(f"Testing background sync enablement for: {username}")
logger.info(f"{'=' * 60}")
logger.info("\\n%s", "=" * 60)
logger.info("Testing background sync enablement for: %s", username)
logger.info("%s", "=" * 60)
user_config = test_users_setup[username]
password = user_config["password"]
@@ -994,17 +1015,20 @@ async def test_multi_user_astrolabe_background_sync_enablement(
"background_sync_active": sync_enabled and app_password_stored,
}
logger.info(f"\n{username} results:")
logger.info("\\n%s results:", username)
logger.info(" Settings accessed: ✓")
logger.info(f" App password generated: {'' if app_password else ''}")
logger.info(f" Sync enabled: {'' if sync_enabled else ''}")
logger.info(f" App password stored: {'' if app_password_stored else ''}")
logger.info(" App password generated: %s", "" if app_password else "")
logger.info(" Sync enabled: %s", "" if sync_enabled else "")
logger.info(
f" Background sync active: {'' if (sync_enabled and app_password_stored) else ''}"
" App password stored: %s", "" if app_password_stored else ""
)
logger.info(
" Background sync active: %s",
"" if (sync_enabled and app_password_stored) else "",
)
except Exception as e:
logger.error(f"Error during {username} test: {e}")
logger.error("Error during %s test: %s", username, e)
results[username] = {
"settings_accessed": False,
"app_password_generated": False,
@@ -1018,18 +1042,18 @@ async def test_multi_user_astrolabe_background_sync_enablement(
await context.close()
# Verify all users succeeded
logger.info(f"\n{'=' * 60}")
logger.info("\\n%s", "=" * 60)
logger.info("Test Summary")
logger.info(f"{'=' * 60}")
logger.info("%s", "=" * 60)
for username, result in results.items():
logger.info(f"\n{username}:")
logger.info("\\n%s:", username)
for key, value in result.items():
if key != "error":
status = "" if value else ""
logger.info(f" {key}: {status}")
logger.info(" %s: %s", key, status)
elif value:
logger.info(f" error: {value}")
logger.info(" error: %s", value)
# Assert all users successfully enabled background sync
for username in test_users:
@@ -1051,7 +1075,8 @@ async def test_multi_user_astrolabe_background_sync_enablement(
)
logger.info(
f"\n✓ All {len(test_users)} users successfully enabled background sync via app passwords!"
"\\n✓ All %s users successfully enabled background sync via app passwords!",
len(test_users),
)
@@ -1065,7 +1090,7 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
Returns:
True if revocation was successful
"""
logger.info(f"Revoking background sync access for {username}...")
logger.info("Revoking background sync access for %s...", username)
nextcloud_url = "http://localhost:8080"
@@ -1080,7 +1105,7 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
def log_response(resp):
response_info = f"{resp.status} {resp.url}"
network_responses.append(response_info)
logger.info(f"Response: {response_info}")
logger.info("Response: %s", response_info)
def log_console(msg):
console_messages.append(f"[{msg.type}] {msg.text}")
@@ -1102,11 +1127,11 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
active_text = page.get_by_text("Active", exact=True)
if not await active_text.is_visible(timeout=2000):
logger.warning(
f"Background sync not active for {username}, nothing to revoke"
"Background sync not active for %s, nothing to revoke", username
)
return False
except Exception:
logger.warning(f"Could not find Active badge for {username}")
logger.warning("Could not find Active badge for %s", username)
return False
# Find the "Revoke Access" button
@@ -1134,21 +1159,21 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
await anyio.sleep(2)
# Log network requests after clicking
logger.info(f"Network requests after Revoke for {username}:")
logger.info("Network requests after Revoke for %s:", username)
for req in network_requests[-10:]:
logger.info(f" {req}")
logger.info(" %s", req)
# Log network responses
logger.info(f"Network responses after Revoke for {username}:")
logger.info("Network responses after Revoke for %s:", username)
for resp in network_responses[-10:]:
logger.info(f" {resp}")
logger.info(" %s", resp)
# Check specifically for the revoke POST response
revoke_responses = [r for r in network_responses if "credentials/revoke" in r]
if revoke_responses:
logger.info(f"Revoke endpoint response: {revoke_responses[-1]}")
logger.info("Revoke endpoint response: %s", revoke_responses[-1])
if "200" not in revoke_responses[-1]:
logger.error(f"Revoke POST did not return 200 OK: {revoke_responses[-1]}")
logger.error("Revoke POST did not return 200 OK: %s", revoke_responses[-1])
return False
else:
logger.warning("No response found for credentials/revoke endpoint!")
@@ -1159,16 +1184,16 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
# Log any console messages
if console_messages:
logger.info(f"Console messages for {username}:")
logger.info("Console messages for %s:", username)
for msg in console_messages:
logger.info(f" {msg}")
logger.info(" %s", msg)
# Check for error notifications (toast messages)
try:
error_toast = page.locator(".toastify.toast-error, .toast-error")
if await error_toast.count() > 0:
error_text = await error_toast.first.text_content()
logger.error(f"Error notification for {username}: {error_text}")
logger.error("Error notification for %s: %s", username, error_text)
return False
except Exception:
pass
@@ -1177,14 +1202,14 @@ async def revoke_background_sync_access(page: Page, username: str) -> bool:
try:
active_text = page.get_by_text("Active", exact=True)
if await active_text.is_visible(timeout=2000):
logger.error(f"Active badge still visible for {username} after revoke!")
logger.error("Active badge still visible for %s after revoke!", username)
screenshot_path = f"/tmp/astrolabe_revoke_still_active_{username}.png"
await page.screenshot(path=screenshot_path)
return False
except Exception:
pass
logger.info(f"✓ Background sync access revoked for {username}")
logger.info("✓ Background sync access revoked for %s", username)
return True
@@ -1197,7 +1222,7 @@ async def verify_app_password_deleted(username: str) -> bool:
Returns:
True if background sync credentials no longer exist
"""
logger.info(f"Verifying background sync credentials deleted for {username}...")
logger.info("Verifying background sync credentials deleted for %s...", username)
query = f"""
SELECT userid, configkey, configvalue
@@ -1230,18 +1255,20 @@ async def verify_app_password_deleted(username: str) -> bool:
)
output = result.stdout
logger.debug(f"Background sync credentials query result:\n{output}")
logger.debug("Background sync credentials query result:\\n%s", output)
# After deletion, we should NOT see background_sync_password
if "background_sync_password" not in output:
logger.info(f"✓ Background sync credentials deleted for {username}")
logger.info("✓ Background sync credentials deleted for %s", username)
return True
else:
logger.warning(f"Background sync credentials still exist for {username}")
logger.warning("Background sync credentials still exist for %s", username)
return False
except Exception as e:
logger.error(f"Error checking background sync credentials for {username}: {e}")
logger.error(
"Error checking background sync credentials for %s: %s", username, e
)
return False
@@ -1316,7 +1343,9 @@ async def test_revoke_background_sync_access(
f"Background sync credentials not deleted for {username}"
)
logger.info(f"\n✓ Successfully revoked background sync access for {username}!")
logger.info(
"\\n✓ Successfully revoked background sync access for %s!", username
)
finally:
await context.close()
@@ -58,7 +58,7 @@ async def wait_for_vector_sync(
while waited < timeout_seconds:
sync_status = await mcp_client.call_tool("nc_get_vector_sync_status", {})
if sync_status.isError:
logger.warning(f"Vector sync status error: {sync_status}")
logger.warning("Vector sync status error: %s", sync_status)
return False, None
status_data = json.loads(sync_status.content[0].text)
@@ -66,14 +66,18 @@ async def wait_for_vector_sync(
pending_count = status_data.get("pending_count", 1)
logger.info(
f"Sync status at {waited}s: indexed={indexed_count}, "
f"pending={pending_count}, status={status_data.get('status')}"
"Sync status at %ss: indexed=%s, pending=%s, status=%s",
waited,
indexed_count,
pending_count,
status_data.get("status"),
)
if indexed_count > initial_indexed_count and pending_count == 0:
logger.info(
f"✓ Sync complete: {indexed_count} documents indexed "
f"(was {initial_indexed_count})"
"✓ Sync complete: %s documents indexed (was %s)",
indexed_count,
initial_indexed_count,
)
return True, status_data
@@ -142,7 +146,7 @@ async def test_astrolabe_plotly_visualization_with_basic_auth(
# Phase 2: Complete full Astrolabe authorization (OAuth + app password)
await login_to_nextcloud(page, username, password)
auth_result = await complete_astrolabe_authorization(page, username, password)
logger.info(f"Authorization result: {auth_result}")
logger.info("Authorization result: %s", auth_result)
# Create MCP client session as alice - all MCP operations inside this block
async with create_mcp_client_session(
@@ -160,7 +164,7 @@ async def test_astrolabe_plotly_visualization_with_basic_auth(
initial_data = json.loads(initial_sync.content[0].text)
initial_count = initial_data.get("indexed_count", 0)
logger.info(f"Initial indexed count: {initial_count}")
logger.info("Initial indexed count: %s", initial_count)
# Create note with unique searchable term
unique_term = f"plotly_viz_test_{uuid.uuid4().hex[:8]}"
@@ -189,7 +193,7 @@ The visualization should show this document as a point in PCA-reduced space.
note_data = json.loads(note_response.content[0].text)
note_id = note_data.get("id")
logger.info(f"Created test note ID: {note_id}")
logger.info("Created test note ID: %s", note_id)
# Phase 4: Wait for vector indexing
sync_complete, status = await wait_for_vector_sync(
@@ -205,7 +209,7 @@ The visualization should show this document as a point in PCA-reduced space.
search_input = page.locator(".mcp-search-input input")
await search_input.wait_for(timeout=10000, state="visible")
await search_input.fill(unique_term)
logger.info(f"Entered search query: {unique_term}")
logger.info("Entered search query: %s", unique_term)
# Trigger search by pressing Enter on the input field
# This is wired to performSearch via @keyup.enter in the Vue component
@@ -246,7 +250,7 @@ The visualization should show this document as a point in PCA-reduced space.
for attempt in range(60): # 60 attempts, 500ms each = 30s total
if await error_note.count() > 0:
error_text = await error_note.text_content()
logger.error(f"Search error: {error_text}")
logger.error("Search error: %s", error_text)
pytest.fail(f"Search failed with error: {error_text}")
if await no_results_text.count() > 0:
@@ -261,13 +265,13 @@ The visualization should show this document as a point in PCA-reduced space.
if await results_text_pattern.count() > 0:
results_text = await results_text_pattern.first.text_content()
logger.info(f"Found results: {results_text}")
logger.info("Found results: %s", results_text)
found_state = True
break
if attempt % 10 == 0:
logger.info(
f"Waiting for results... (attempt {attempt + 1}/60)"
"Waiting for results... (attempt %s/60)", attempt + 1
)
await anyio.sleep(0.5)
@@ -275,8 +279,8 @@ The visualization should show this document as a point in PCA-reduced space.
if not found_state:
await page.screenshot(path="/tmp/astrolabe_search_timeout.png")
page_content = await page.content()
logger.error(f"Search state not resolved. Page URL: {page.url}")
logger.error(f"Page content snippet: {page_content[:2000]}")
logger.error("Search state not resolved. Page URL: %s", page.url)
logger.error("Page content snippet: %s", page_content[:2000])
raise AssertionError("Search did not complete within timeout")
except AssertionError:
@@ -285,8 +289,8 @@ The visualization should show this document as a point in PCA-reduced space.
# Take another screenshot and get page content for debugging
await page.screenshot(path="/tmp/astrolabe_search_timeout.png")
page_content = await page.content()
logger.error(f"Search state not resolved. Page URL: {page.url}")
logger.error(f"Page content snippet: {page_content[:2000]}")
logger.error("Search state not resolved. Page URL: %s", page.url)
logger.error("Page content snippet: %s", page_content[:2000])
raise AssertionError(f"Search did not complete: {e}")
logger.info("Results loaded")
@@ -316,7 +320,7 @@ The visualization should show this document as a point in PCA-reduced space.
result_items = page.locator(".mcp-result-item")
result_count = await result_items.count()
assert result_count > 0, "No search results displayed"
logger.info(f"✓ Found {result_count} search result(s)")
logger.info("✓ Found %s search result(s)", result_count)
# Verify our note appears in results
found_note = False
@@ -326,7 +330,7 @@ The visualization should show this document as a point in PCA-reduced space.
title_text = await title_elem.text_content()
if title_text and unique_term in title_text:
found_note = True
logger.info(f"✓ Found test note in results: {title_text}")
logger.info("✓ Found test note in results: %s", title_text)
break
assert found_note, f"Created note with '{unique_term}' not found in results"
@@ -342,14 +346,14 @@ The visualization should show this document as a point in PCA-reduced space.
"nc_notes_delete_note", {"note_id": note_id}
)
if not delete_response.isError:
logger.info(f"✓ Cleaned up test note {note_id}")
logger.info("✓ Cleaned up test note %s", note_id)
note_id = None # Mark as cleaned
else:
logger.warning(
f"Failed to delete note {note_id}: {delete_response}"
"Failed to delete note %s: %s", note_id, delete_response
)
except Exception as e:
logger.warning(f"Cleanup failed for note {note_id}: {e}")
logger.warning("Cleanup failed for note %s: %s", note_id, e)
finally:
# Cleanup note if not already cleaned (create new client for cleanup)
@@ -364,13 +368,13 @@ The visualization should show this document as a point in PCA-reduced space.
"nc_notes_delete_note", {"note_id": note_id}
)
if not delete_response.isError:
logger.info(f"✓ Cleaned up test note {note_id} (finally)")
logger.info("✓ Cleaned up test note %s (finally)", note_id)
else:
logger.warning(
f"Failed to delete note {note_id}: {delete_response}"
"Failed to delete note %s: %s", note_id, delete_response
)
except Exception as e:
logger.warning(f"Cleanup failed for note {note_id}: {e}")
logger.warning("Cleanup failed for note %s: %s", note_id, e)
# Close browser context
await context.close()
@@ -45,7 +45,7 @@ async def login_to_nextcloud(page: Page, username: str, password: str):
"""
nextcloud_url = "http://localhost:8080"
logger.info(f"Logging in to Nextcloud as {username}...")
logger.info("Logging in to Nextcloud as %s...", username)
await page.goto(f"{nextcloud_url}/login", wait_until="networkidle")
# Fill in login form
@@ -62,7 +62,7 @@ async def login_to_nextcloud(page: Page, username: str, password: str):
assert "/login" not in current_url, (
f"Login failed for {username}, still on login page"
)
logger.info(f"✓ Successfully logged in as {username}")
logger.info("✓ Successfully logged in as %s", username)
async def generate_app_password(
@@ -78,7 +78,7 @@ async def generate_app_password(
Returns:
The generated app password string
"""
logger.info(f"Generating app password for {username}...")
logger.info("Generating app password for %s...", username)
nextcloud_url = "http://localhost:8080"
@@ -89,7 +89,7 @@ async def generate_app_password(
# Fill the app password input field
app_password_input = page.locator('input[placeholder="App name"]')
await app_password_input.fill(app_name)
logger.info(f"Entered app name: {app_name}")
logger.info("Entered app name: %s", app_name)
# Wait for Vue.js to react and enable the button
await anyio.sleep(1.0)
@@ -116,12 +116,12 @@ async def generate_app_password(
value = await input_elem.input_value()
if value and "-" in value and len(value) > 20:
app_password = value.strip()
logger.info(f"Found app password in input {idx}")
logger.info("Found app password in input %s", idx)
break
except Exception:
continue
except Exception as e:
logger.error(f"Failed to find app password dialog: {e}")
logger.error("Failed to find app password dialog: %s", e)
if not app_password:
screenshot_path = f"/tmp/app_password_generation_{username}.png"
@@ -137,7 +137,7 @@ async def generate_app_password(
):
raise ValueError(f"App password format validation failed: {app_password}")
logger.info(f"✓ Generated app password for {username}")
logger.info("✓ Generated app password for %s", username)
# Close the dialog
close_button = page.get_by_role("button", name="Close")
@@ -163,7 +163,7 @@ async def save_app_password_in_astrolabe(
Returns:
True if the password was saved successfully (based on network response)
"""
logger.info(f"Saving app password in Astrolabe for {username}...")
logger.info("Saving app password in Astrolabe for %s...", username)
nextcloud_url = "http://localhost:8080"
@@ -174,7 +174,7 @@ async def save_app_password_in_astrolabe(
nonlocal credentials_response_status
if "background-sync/credentials" in resp.url or "storeAppPassword" in resp.url:
credentials_response_status = resp.status
logger.info(f"Credentials endpoint response: {resp.status} {resp.url}")
logger.info("Credentials endpoint response: %s %s", resp.status, resp.url)
page.on("response", capture_response)
@@ -188,7 +188,7 @@ async def save_app_password_in_astrolabe(
try:
complete_badge = page.locator('text="Complete"').first
if await complete_badge.is_visible(timeout=2000):
logger.info(f"✓ App password already configured for {username}")
logger.info("✓ App password already configured for %s", username)
return True
except Exception:
pass
@@ -208,7 +208,7 @@ async def save_app_password_in_astrolabe(
# Enter the app password
await app_password_input.fill(app_password)
logger.info(f"Entered app password for {username}")
logger.info("Entered app password for %s", username)
await anyio.sleep(0.5)
@@ -223,11 +223,13 @@ async def save_app_password_in_astrolabe(
# Verify the save was successful by checking network response
if credentials_response_status == 200:
logger.info(f"✓ App password saved successfully for {username}")
logger.info("✓ App password saved successfully for %s", username)
return True
else:
logger.error(
f"App password save failed for {username}, status: {credentials_response_status}"
"App password save failed for %s, status: %s",
username,
credentials_response_status,
)
screenshot_path = f"/tmp/astrolabe_save_failed_{username}.png"
await page.screenshot(path=screenshot_path)
@@ -284,7 +286,7 @@ def get_background_sync_credentials(username: str) -> dict | None:
return None
except Exception as e:
logger.error(f"Error getting credentials for {username}: {e}")
logger.error("Error getting credentials for %s: %s", username, e)
return None
@@ -325,11 +327,11 @@ def delete_user_credentials(username: str) -> bool:
timeout=10,
)
logger.info(f"Deleted credentials for {username}")
logger.info("Deleted credentials for %s", username)
return result.returncode == 0
except Exception as e:
logger.error(f"Error deleting credentials for {username}: {e}")
logger.error("Error deleting credentials for %s: %s", username, e)
return False
@@ -489,7 +491,7 @@ async def test_credential_isolation_between_users(
# Verify stored
creds = get_background_sync_credentials(username)
assert creds is not None, f"Credentials not stored for {username}"
logger.info(f"✓ Credentials provisioned for {username}")
logger.info("✓ Credentials provisioned for %s", username)
finally:
await context.close()
+16 -14
View File
@@ -26,7 +26,7 @@ async def board_with_two_stacks(nc_client: NextcloudClient):
board_title = f"Reorder Test Board {unique_suffix}"
board = None
logger.info(f"Creating board with two stacks: {board_title}")
logger.info("Creating board with two stacks: %s", board_title)
try:
board = await nc_client.deck.create_board(board_title, "0000FF")
board_id = board.id
@@ -40,7 +40,7 @@ async def board_with_two_stacks(nc_client: NextcloudClient):
"title": source_stack.title,
"order": source_stack.order,
}
logger.info(f"Created source stack with ID: {source_stack.id}")
logger.info("Created source stack with ID: %s", source_stack.id)
# Create target stack (stack 2)
target_stack = await nc_client.deck.create_stack(
@@ -51,7 +51,7 @@ async def board_with_two_stacks(nc_client: NextcloudClient):
"title": target_stack.title,
"order": target_stack.order,
}
logger.info(f"Created target stack with ID: {target_stack.id}")
logger.info("Created target stack with ID: %s", target_stack.id)
board_data = {
"id": board_id,
@@ -63,11 +63,11 @@ async def board_with_two_stacks(nc_client: NextcloudClient):
finally:
if board:
logger.info(f"Cleaning up board ID: {board.id}")
logger.info("Cleaning up board ID: %s", board.id)
try:
await nc_client.deck.delete_board(board.id)
except Exception as e:
logger.warning(f"Error cleaning up board: {e}")
logger.warning("Error cleaning up board: %s", e)
async def test_reorder_card_move_to_different_stack(
@@ -90,7 +90,7 @@ async def test_reorder_card_move_to_different_stack(
board_id, source_stack_id, card_title, description="Card to be moved"
)
card_id = card.id
logger.info(f"Created card ID: {card_id} in source stack ID: {source_stack_id}")
logger.info("Created card ID: %s in source stack ID: %s", card_id, source_stack_id)
try:
# Verify card is in source stack
@@ -99,12 +99,14 @@ async def test_reorder_card_move_to_different_stack(
f"Card should start in source stack {source_stack_id}, "
f"but is in {card_before.stackId}"
)
logger.info(f"Verified card is in source stack: {source_stack_id}")
logger.info("Verified card is in source stack: %s", source_stack_id)
# Move card to target stack
logger.info(
f"Moving card {card_id} from stack {source_stack_id} "
f"to stack {target_stack_id}"
"Moving card %s from stack %s to stack %s",
card_id,
source_stack_id,
target_stack_id,
)
await nc_client.deck.reorder_card(
board_id=board_id,
@@ -122,7 +124,7 @@ async def test_reorder_card_move_to_different_stack(
f"Card should have moved to target stack {target_stack_id}, "
f"but is in {card_after.stackId}"
)
logger.info(f"SUCCESS: Card moved to target stack {target_stack_id}")
logger.info("SUCCESS: Card moved to target stack %s", target_stack_id)
finally:
# Clean up - try to delete from target stack first, then source
@@ -132,7 +134,7 @@ async def test_reorder_card_move_to_different_stack(
try:
await nc_client.deck.delete_card(board_id, source_stack_id, card_id)
except Exception as e:
logger.warning(f"Error cleaning up card: {e}")
logger.warning("Error cleaning up card: %s", e)
async def test_reorder_card_within_same_stack(
@@ -151,7 +153,7 @@ async def test_reorder_card_within_same_stack(
card2 = await nc_client.deck.create_card(
board_id, source_stack_id, f"Card 2 {unique_suffix}", order=1
)
logger.info(f"Created cards {card1.id} (order 0) and {card2.id} (order 1)")
logger.info("Created cards %s (order 0) and %s (order 1)", card1.id, card2.id)
try:
# Reorder card1 to position after card2
@@ -162,7 +164,7 @@ async def test_reorder_card_within_same_stack(
order=2, # Move to position 2
target_stack_id=source_stack_id, # Same stack
)
logger.info(f"Reordered card {card1.id} to order 2")
logger.info("Reordered card %s to order 2", card1.id)
# Verify card is still in the same stack
card_after = await nc_client.deck.get_card(board_id, source_stack_id, card1.id)
@@ -174,4 +176,4 @@ async def test_reorder_card_within_same_stack(
await nc_client.deck.delete_card(board_id, source_stack_id, card1.id)
await nc_client.deck.delete_card(board_id, source_stack_id, card2.id)
except Exception as e:
logger.warning(f"Error cleaning up cards: {e}")
logger.warning("Error cleaning up cards: %s", e)
+12 -9
View File
@@ -129,7 +129,7 @@ async def indexed_manual_pdf(nc_client, nc_mcp_client):
manual_path = os.getenv("RAG_MANUAL_PATH", DEFAULT_MANUAL_PATH)
logger.info(f"Setting up indexed manual PDF: {manual_path}")
logger.info("Setting up indexed manual PDF: %s", manual_path)
# Get file info to verify file exists and get file ID. After the
# round-7 contract widening, get_file_info raises HTTPStatusError on
@@ -145,16 +145,16 @@ async def indexed_manual_pdf(nc_client, nc_mcp_client):
pytest.skip(f"Manual PDF unreadable at '{manual_path}' (malformed PROPFIND)")
file_id = file_info["id"]
logger.info(f"Found manual PDF: {manual_path} (file_id={file_id})")
logger.info("Found manual PDF: %s (file_id=%s)", manual_path, file_id)
# Create or get the vector-index tag
tag = await nc_client.webdav.get_or_create_tag("vector-index")
tag_id = tag["id"]
logger.info(f"Using tag 'vector-index' (tag_id={tag_id})")
logger.info("Using tag 'vector-index' (tag_id=%s)", tag_id)
# Assign tag to file
await nc_client.webdav.assign_tag_to_file(file_id, tag_id)
logger.info(f"Tagged file {file_id} with vector-index tag")
logger.info("Tagged file %s with vector-index tag", file_id)
# Wait for vector sync to complete indexing
max_attempts = 60
@@ -176,23 +176,26 @@ async def indexed_manual_pdf(nc_client, nc_mcp_client):
pending = content.get("pending_count", 1)
logger.info(
f"Attempt {attempt}/{max_attempts}: "
f"indexed={indexed}, pending={pending}"
"Attempt %s/%s: indexed=%s, pending=%s",
attempt,
max_attempts,
indexed,
pending,
)
if indexed > 0 and pending == 0:
logger.info(
f"Vector indexing complete: {indexed} documents indexed"
"Vector indexing complete: %s documents indexed", indexed
)
break
except Exception as e:
logger.warning(f"Attempt {attempt}: Error checking status: {e}")
logger.warning("Attempt %s: Error checking status: %s", attempt, e)
if attempt < max_attempts:
await anyio.sleep(poll_interval)
else:
logger.warning(
f"Vector indexing may not be complete after {max_attempts} attempts"
"Vector indexing may not be complete after %s attempts", max_attempts
)
yield {
+2 -2
View File
@@ -57,7 +57,7 @@ async def test_unstructured_api_enabled_parsing(
await nc_client.webdav.write_file(
test_file, pdf_content, content_type="application/pdf"
)
logger.info(f"Uploaded PDF file: {test_file}")
logger.info("Uploaded PDF file: %s", test_file)
# Read the PDF using MCP tool (should parse via Unstructured API)
mcp_result = await nc_mcp_client.call_tool(
@@ -123,7 +123,7 @@ async def test_unstructured_api_with_docx(
docx_content,
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
logger.info(f"Uploaded DOCX file: {test_file}")
logger.info("Uploaded DOCX file: %s", test_file)
# Read the file using MCP tool
mcp_result = await nc_mcp_client.call_tool(