Skip to main content
Glama
test_viewer_storage.py29 kB
"""Cross-browser storage tests for OPFS caching and offline functionality. This test suite documents requirements for testing the viewer's offline storage capabilities: 1. OPFS (Origin Private File System) caching 2. Cache version management and invalidation 3. Cross-origin isolation requirements 4. SharedArrayBuffer availability 5. Warm vs cold load performance Reference: PLAN_TO_ENABLE_EASY_AND_SECURE_SHARING_OF_AGENT_MAILBOX.md line 262 Note: Comprehensive cross-browser testing requires browser automation (Playwright/Puppeteer) as OPFS and SharedArrayBuffer APIs are not available in Node.js or Python runtimes. """ from __future__ import annotations def test_opfs_caching_requirements_documentation() -> None: """Document requirements for OPFS caching tests. OPFS (Origin Private File System) is a browser API for persistent local storage that enables faster warm-load performance by caching the SQLite database locally. Testing OPFS requires real browsers with the following conditions: 1. Cross-origin isolation enabled (COOP + COEP headers) 2. Chromium-based browser (Chromium 102+, Chrome, Edge) 3. HTTPS or localhost origin """ documentation = """ OPFS Caching Test Requirements =============================== The static viewer uses OPFS for offline caching when available. To test OPFS: Required Browser Conditions: --------------------------- 1. Cross-Origin Isolation: Page must be served with headers: - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp 2. Browser Support: - Chrome/Chromium 102+ - Edge 102+ - Opera 88+ - Firefox: Limited support (requires flags) - Safari: Not supported as of 2025 3. Secure Context: - HTTPS origin OR localhost for development Test Scenarios: --------------- 1. Cold Load (First Visit) -------------------------- - Navigate to viewer URL - Verify: Database is fetched from network - Verify: OPFS write occurs (console logs) - Verify: Cache state = "opfs" in diagnostics - Verify: Cache button shows "Cache for offline use" - Measure: Time to first meaningful paint Expected behavior: - Database downloaded from network - Written to OPFS: /opfs-root/mailbox-snapshot-{cacheKey}.sqlite3 - Metadata written: /opfs-root/mailbox-snapshot-{cacheKey}.meta.json - Viewer functional after download completes 2. Warm Load (Subsequent Visit) ------------------------------- - Refresh page or close/reopen browser - Verify: Database loaded from OPFS (console logs) - Verify: No network requests for .sqlite3 - Verify: Cache state = "opfs" in diagnostics - Measure: Significantly faster load time (< 500ms typical) Expected behavior: - Database read from OPFS cache - No network download - Immediate viewer functionality 3. Cache Invalidation (Version Mismatch) ---------------------------------------- - Deploy updated bundle with new cacheKey (hash changes) - Reload page - Verify: Old cache detected as stale - Verify: Old cache files deleted - Verify: New database downloaded and cached Expected behavior: - Console warning: "Stale OPFS cache detected, invalidating" - Old .sqlite3 and .meta.json files removed - Fresh download and re-cache 4. Cache Manual Clear --------------------- - Open diagnostics panel - Click "Clear All Caches" - Verify: OPFS files deleted - Verify: Next load fetches from network Expected behavior: - All mailbox-snapshot-* files removed from OPFS - Cache state reset to "memory" or "none" - Alert confirms cache cleared 5. Cross-Origin Isolation Fallback ----------------------------------- - Serve viewer without COOP/COEP headers - Navigate to viewer - Verify: OPFS unavailable warning - Verify: Cache state = "unsupported" or "memory" - Verify: Viewer still functional (in-memory mode) Expected behavior: - Console warning about cross-origin isolation - Cache toggle disabled - Database loaded into memory only - No persistent cache 6. Offline Functionality ------------------------ - Load viewer with OPFS cache populated - Disconnect network / enable offline mode - Refresh page - Verify: Viewer loads from OPFS cache - Verify: Full functionality maintained Expected behavior: - Page loads without network requests - All messages visible - Search functional - No degradation Performance Targets: ------------------- - Cold load (100 MB DB): < 5 seconds to interactive - Warm load (100 MB DB): < 500ms to interactive - OPFS write speed: > 50 MB/s - OPFS read speed: > 100 MB/s """ assert len(documentation) > 500, "OPFS documentation should be comprehensive" def test_playwright_opfs_test_example() -> None: """Provide example Playwright test for OPFS functionality. This test shows how to implement OPFS caching tests using Playwright. To run these tests, install playwright and create a tests/playwright/ directory. """ example_code = ''' """Example Playwright test for OPFS caching.""" import pytest from playwright.sync_api import Page, expect @pytest.fixture def viewer_page(page: Page): """Launch viewer with cross-origin isolation headers.""" # Start local server with proper headers # See src/mcp_agent_mail/share.py start_preview_server for header config page.goto("http://localhost:8000/viewer/index.html") # Wait for viewer to initialize page.wait_for_selector("#message-list", timeout=10000) return page def test_opfs_cold_load(viewer_page: Page): """Test initial load caches database to OPFS.""" # Check cross-origin isolation is_isolated = viewer_page.evaluate("() => window.crossOriginIsolated") assert is_isolated, "Cross-origin isolation required for OPFS" # Open diagnostics viewer_page.click("#diagnostics-toggle") # Verify OPFS is available opfs_status = viewer_page.text_content("#diag-opfs-status") assert "✅" in opfs_status, "OPFS should be available" # Verify cache state after load cache_state = viewer_page.text_content("#diag-cache-state") assert "OPFS" in cache_state or "memory" in cache_state def test_opfs_warm_load_performance(viewer_page: Page): """Test that warm load is significantly faster than cold load.""" # First load (cold) - should cache to OPFS viewer_page.goto("http://localhost:8000/viewer/index.html") viewer_page.wait_for_selector("#message-list li", timeout=10000) cold_load_time = viewer_page.evaluate( "() => performance.getEntriesByType('navigation')[0].loadEventEnd" ) # Second load (warm) - should load from OPFS viewer_page.reload() viewer_page.wait_for_selector("#message-list li", timeout=10000) warm_load_time = viewer_page.evaluate( "() => performance.getEntriesByType('navigation')[0].loadEventEnd" ) print(f"Cold load: {cold_load_time}ms") print(f"Warm load: {warm_load_time}ms") # Warm load should be at least 2x faster assert warm_load_time < cold_load_time / 2, "Warm load should use OPFS cache" def test_opfs_cache_invalidation(viewer_page: Page): """Test that cache is invalidated when cacheKey changes.""" # Load initial version viewer_page.goto("http://localhost:8000/viewer/index.html") viewer_page.wait_for_selector("#message-list", timeout=10000) # Get initial cache key from diagnostics viewer_page.click("#diagnostics-toggle") initial_key = viewer_page.text_content("#diag-cache-key") # Simulate new bundle deployment by modifying manifest.json # (In real scenario, deploy new bundle with different hash) # Reload page viewer_page.reload() viewer_page.wait_for_selector("#message-list", timeout=10000) # Verify console shows invalidation messages = viewer_page.evaluate("() => console.messages") has_invalidation = any("stale" in str(m).lower() for m in messages) # Note: Actual implementation depends on test infrastructure def test_opfs_offline_functionality(viewer_page: Page): """Test that viewer works offline after caching to OPFS.""" # Load page and verify OPFS cache viewer_page.goto("http://localhost:8000/viewer/index.html") viewer_page.wait_for_selector("#message-list li", timeout=10000) # Enable offline mode context = viewer_page.context context.set_offline(True) # Reload page viewer_page.reload() # Verify page loads from cache viewer_page.wait_for_selector("#message-list li", timeout=10000) # Verify messages are visible message_count = viewer_page.locator("#message-list li").count() assert message_count > 0, "Messages should load from OPFS cache offline" # Disable offline mode context.set_offline(False) def test_cross_origin_isolation_fallback(page: Page): """Test viewer works without cross-origin isolation (memory-only mode).""" # Navigate to viewer served WITHOUT COOP/COEP headers # This requires separate server configuration for testing page.goto("http://localhost:8001/viewer/index.html") # Server without headers # Wait for viewer to load page.wait_for_selector("#message-list", timeout=10000) # Verify cross-origin isolation is disabled is_isolated = page.evaluate("() => window.crossOriginIsolated") assert not is_isolated, "Should not be cross-origin isolated" # Open diagnostics page.click("#diagnostics-toggle") # Verify OPFS shows as unavailable opfs_status = page.text_content("#diag-opfs-status") assert "❌" in opfs_status, "OPFS should be unavailable without isolation" # Verify viewer still works (memory mode) cache_state = page.text_content("#diag-cache-state") assert "memory" in cache_state.lower() or "unsupported" in cache_state.lower() # Verify messages are visible message_count = page.locator("#message-list li").count() assert message_count > 0, "Viewer should work in memory-only mode" def test_cache_clear_functionality(viewer_page: Page): """Test manual cache clearing through diagnostics.""" # Open diagnostics panel viewer_page.click("#diagnostics-toggle") # Click clear all caches button viewer_page.click("#clear-all-caches") # Handle confirmation dialog viewer_page.on("dialog", lambda dialog: dialog.accept()) # Wait for alert confirmation # Note: Actual implementation depends on how alerts are handled # Verify cache state updated cache_state = viewer_page.text_content("#diag-cache-state") assert "memory" in cache_state.lower() or "none" in cache_state.lower() # Reload and verify re-download viewer_page.reload() viewer_page.wait_for_selector("#message-list", timeout=10000) ''' assert len(example_code) > 1000, "Example code should be comprehensive" def test_sharedarraybuffer_requirements_documentation() -> None: """Document SharedArrayBuffer requirements for sql.js performance. SharedArrayBuffer enables sqlite-wasm to use optimized memory access patterns, improving query performance. It requires cross-origin isolation. """ documentation = """ SharedArrayBuffer Requirements for sql.js Performance ===================================================== The viewer uses sql.js (SQLite compiled to WebAssembly) which can leverage SharedArrayBuffer for improved performance when available. Required Conditions: ------------------- 1. Cross-Origin Isolation (same as OPFS): - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp 2. Browser Support: - Chrome 92+ - Firefox 79+ - Safari 15.2+ - Edge 92+ 3. Secure Context: - HTTPS or localhost Test Scenarios: --------------- 1. Verify SharedArrayBuffer Availability ---------------------------------------- Open browser console: ```javascript typeof SharedArrayBuffer !== 'undefined' // Should return: true (with cross-origin isolation) // false (without isolation) ``` Open diagnostics panel: - Look for "SharedArrayBuffer: ✅ Available" 2. Performance With vs Without SharedArrayBuffer ------------------------------------------------ Compare query performance: - With SAB: Queries may be 10-30% faster - Without SAB: Still functional, slightly slower Benchmark: Run search query on large database - With SAB: < 100ms for FTS search - Without SAB: < 150ms for FTS search 3. Fallback Behavior -------------------- When SharedArrayBuffer unavailable: - sql.js falls back to regular memory - No functionality loss - Slight performance degradation - Viewer remains fully functional Performance Monitoring: ---------------------- Use diagnostics panel to check: - Bootstrap time should be < 2 seconds - FTS search should be < 200ms - Message detail load should be < 50ms If performance degrades without SharedArrayBuffer: - Enable cross-origin isolation - Verify headers are properly set - Check browser version supports SAB """ assert len(documentation) > 500, "SharedArrayBuffer documentation should be comprehensive" def test_browser_compatibility_matrix_documentation() -> None: """Document browser compatibility for OPFS and SharedArrayBuffer features.""" documentation = """ Browser Compatibility Matrix for Viewer Features ================================================= Feature: OPFS (Origin Private File System) ------------------------------------------- | Browser | Version | Support | Notes | |------------------|---------|---------|------------------------------| | Chrome | 102+ | ✅ Full | Stable since M102 | | Edge | 102+ | ✅ Full | Chromium-based | | Opera | 88+ | ✅ Full | Chromium-based | | Brave | 1.40+ | ✅ Full | Chromium-based | | Firefox | 111+ | ⚠️ Flag | Requires about:config flag | | Safari | N/A | ❌ None | Not implemented | | Safari iOS | N/A | ❌ None | Not implemented | | Samsung Internet | 19+ | ✅ Full | Chromium-based | Feature: SharedArrayBuffer --------------------------- | Browser | Version | Support | Notes | |------------------|---------|---------|------------------------------| | Chrome | 92+ | ✅ Full | Requires cross-origin iso. | | Edge | 92+ | ✅ Full | Requires cross-origin iso. | | Firefox | 79+ | ✅ Full | Requires cross-origin iso. | | Safari | 15.2+ | ✅ Full | Requires cross-origin iso. | | Opera | 78+ | ✅ Full | Requires cross-origin iso. | Feature: WebAssembly (sql.js requirement) ------------------------------------------ | Browser | Version | Support | Notes | |------------------|---------|---------|------------------------------| | All modern | 2017+ | ✅ Full | Universal support | Recommended Testing Configuration: ---------------------------------- - Primary: Chrome 120+ (best OPFS support) - Secondary: Firefox 120+ (verify degraded OPFS) - Tertiary: Safari 17+ (verify memory-only mode) Fallback Behavior: ------------------ When OPFS unavailable: - Database cached in memory only - Cache cleared on page unload - Viewer remains fully functional - Performance slightly reduced When SharedArrayBuffer unavailable: - sql.js uses regular memory - Slight performance degradation - All features remain functional Testing Checklist: ------------------ For each browser: 1. ✅ Load viewer with cross-origin isolation 2. ✅ Verify OPFS status in diagnostics 3. ✅ Verify SharedArrayBuffer status 4. ✅ Test cold load (first visit) 5. ✅ Test warm load (cached) 6. ✅ Test cache invalidation 7. ✅ Test offline functionality (if OPFS available) 8. ✅ Test without cross-origin isolation (fallback) 9. ✅ Verify search performance 10. ✅ Verify navigation responsiveness """ assert len(documentation) > 500, "Compatibility matrix should be comprehensive" def test_cache_toggle_functionality_documentation() -> None: """Document the cache toggle button behavior and user interaction. The cache toggle button allows users to explicitly trigger OPFS caching when automatic caching hasn't occurred or when re-caching is needed. """ documentation = """ Cache Toggle Button Behavior ============================= The viewer includes a "Cache for offline use" button that manages OPFS caching. Button States: -------------- 1. Initial State (OPFS Available, Not Cached): Text: "Cache for offline use" Enabled: Yes Action: Downloads DB and writes to OPFS 2. Caching in Progress: Text: "Caching..." Enabled: No Action: Shows progress 3. Cached State (OPFS Active): Text: "✓ Cached offline" Enabled: Yes (allows re-cache) Action: Re-downloads and updates cache 4. OPFS Unavailable: Text: "Cache unavailable (requires cross-origin isolation)" Enabled: No Action: None (informational) 5. Memory-Only Mode (Automatic): Text: "Using memory cache" Enabled: No Action: None (already cached in memory) User Flows: ----------- Flow 1: Manual Cache on First Visit ------------------------------------ 1. User loads viewer 2. Database loads into memory automatically 3. User clicks "Cache for offline use" 4. Database written to OPFS 5. Button updates to "✓ Cached offline" 6. Subsequent visits load from OPFS automatically Flow 2: Automatic Caching (Default) ------------------------------------ 1. User loads viewer 2. Database downloads from network 3. Automatically written to OPFS (if available) 4. Button already shows "✓ Cached offline" 5. No user action needed Flow 3: Re-cache After Update ------------------------------ 1. User has old version cached 2. New bundle deployed (different cacheKey) 3. Viewer detects stale cache 4. Old cache automatically invalidated 5. New database downloaded and cached 6. Button shows "✓ Cached offline" for new version Flow 4: Fallback to Memory (No OPFS) ------------------------------------- 1. User loads viewer without cross-origin isolation 2. OPFS unavailable 3. Database loads into memory only 4. Button disabled with message 5. Viewer fully functional but no persistence Testing the Cache Toggle: -------------------------- Manual Test Steps: 1. Open viewer in Chrome 102+ 2. Verify cross-origin isolation (check diagnostics) 3. Click cache toggle button 4. Monitor console for OPFS write logs 5. Refresh page 6. Verify warm load from OPFS (console logs) 7. Open diagnostics to confirm cache state Expected Console Output (Successful Cache): ``` [viewer] Cache supported, attempting to cache... [viewer] OPFS write successful: mailbox-snapshot-abc123.sqlite3 [viewer] Cache state updated: opfs ``` Expected Console Output (Warm Load): ``` [viewer] Using OPFS cache: mailbox-snapshot-abc123.sqlite3 [viewer] Cache state: opfs [viewer] Bootstrap completed in 324ms ``` Error Scenarios: ---------------- 1. OPFS Quota Exceeded: - Console warning: "OPFS quota exceeded" - Falls back to memory cache - Button shows error state 2. OPFS Write Permission Denied: - Console error: "OPFS permission denied" - Falls back to memory cache - Button disabled 3. Network Failure During Cache: - Console error: "Network error during caching" - Retries may occur - Button shows error state """ assert len(documentation) > 500, "Cache toggle documentation should be comprehensive" def test_performance_metrics_documentation() -> None: """Document performance metrics for storage operations. Provides baseline expectations for OPFS read/write performance and cache effectiveness measurements. """ documentation = """ Storage Performance Metrics and Benchmarks =========================================== Expected Performance (Baseline): --------------------------------- OPFS Write Performance: - 10 MB database: < 200ms - 100 MB database: < 2 seconds - 500 MB database: < 10 seconds - Throughput: 50-100 MB/s typical OPFS Read Performance: - 10 MB database: < 100ms - 100 MB database: < 500ms - 500 MB database: < 2 seconds - Throughput: 100-200 MB/s typical Network Fetch Performance: - 10 MB (gzip compressed ~3 MB): 1-2 seconds on fast connection - 100 MB (gzip compressed ~30 MB): 5-10 seconds on fast connection - httpvfs chunked: Initial load ~5-10 Range requests Cache Effectiveness: ------------------- Warm Load Speedup: - Small bundles (< 10 MB): 5-10x faster - Medium bundles (10-100 MB): 10-20x faster - Large bundles (> 100 MB): 20-50x faster Example Measurements: - 100 MB bundle cold load: 8 seconds - 100 MB bundle warm load: 400ms - Speedup: 20x Performance Monitoring: ----------------------- Use browser DevTools Performance tab: 1. Record load timeline 2. Check "Caching" marks in timeline 3. Measure TTFB (Time to First Byte) 4. Measure FCP (First Contentful Paint) 5. Measure TTI (Time to Interactive) Use diagnostics panel: - Bootstrap time: < 2 seconds target - Database source: "OPFS cache" vs "network" - Cache state: "opfs", "memory", or "none" Performance Regression Detection: --------------------------------- Monitor these metrics over time: - Bootstrap time increasing → investigate cache effectiveness - Warm load time increasing → check OPFS read performance - Cold load time increasing → check network/server performance Target Thresholds (100 MB bundle): - Cold load: < 10 seconds - Warm load: < 500ms - Bootstrap: < 2 seconds - First message visible: < 1 second (warm), < 5 seconds (cold) Browser-Specific Variations: ---------------------------- Chrome/Edge (Chromium): - Best OPFS performance - Consistent read/write speeds - Recommended for testing Firefox: - OPFS behind flag or unavailable - Memory-only mode typical - Good baseline for fallback testing Safari: - No OPFS support - Memory-only mode - Useful for testing degraded experience """ assert len(documentation) > 500, "Performance metrics documentation should be comprehensive" def test_debugging_tools_documentation() -> None: """Document debugging tools for storage and caching issues. Provides guidance on using browser DevTools and diagnostics panel to troubleshoot storage-related problems. """ documentation = """ Debugging Storage and Caching Issues ===================================== Diagnostics Panel: ------------------ Access: Click "Diagnostics" button in viewer banner Key Information: 1. Cross-Origin Isolation Status: - ✅ Enabled → OPFS available - ❌ Disabled → Memory-only mode 2. SharedArrayBuffer Status: - ✅ Available → Optimal sql.js performance - ❌ Unavailable → Fallback mode 3. OPFS Access Status: - ✅ Available → Can use persistent cache - ❌ Unavailable → Memory-only mode 4. Cache State: - "OPFS (persistent)" → Cached to OPFS successfully - "Memory (session only)" → Cached in memory, cleared on exit - "No cache" → No caching active - "Unsupported" → Browser doesn't support required APIs 5. Cache Key: - SHA256 hash of database - Used for cache versioning - Changes when bundle updated 6. Bootstrap Time: - Time from page load to viewer ready - Cold load: 2-10 seconds typical - Warm load: < 500ms typical Browser DevTools - Application Tab: ------------------------------------ Check OPFS Contents: 1. Open DevTools (F12) 2. Go to Application tab 3. Navigate to "Storage" → "OPFS" 4. Look for mailbox-snapshot-*.sqlite3 files 5. Verify file sizes match expected database size Check Storage Quota: 1. Console: `navigator.storage.estimate()` 2. Check quota and usage 3. Verify sufficient space available Example: ```javascript navigator.storage.estimate().then(estimate => { console.log(`Used: ${estimate.usage / 1024 / 1024} MB`); console.log(`Quota: ${estimate.quota / 1024 / 1024} MB`); console.log(`Available: ${(estimate.quota - estimate.usage) / 1024 / 1024} MB`); }); ``` Browser DevTools - Network Tab: -------------------------------- Monitor Caching Behavior: 1. Open DevTools Network tab 2. Disable cache in DevTools (checkbox) 3. Load viewer 4. Check for .sqlite3 download Expected Network Activity: - Cold load: Large .sqlite3 download - Warm load: No .sqlite3 download (from OPFS) - httpvfs: Multiple Range requests Browser DevTools - Console: ---------------------------- Check Viewer Logs: - [viewer] logs show caching operations - Look for OPFS read/write messages - Check for cache invalidation warnings - Monitor performance timings Example Console Output: ``` [viewer] Database loaded from network [viewer] Attempting to cache to OPFS... [viewer] OPFS write successful [viewer] Cache state: opfs [viewer] Bootstrap time: 1234ms ``` Common Issues and Solutions: ---------------------------- Issue: "Cache unavailable" despite Chrome 102+ Solution: - Check cross-origin isolation headers - Verify HTTPS or localhost origin - Check browser flags (chrome://flags) Issue: Warm load still downloads database Solution: - Check if cacheKey changed (bundle updated) - Verify OPFS files in DevTools Application tab - Check console for cache invalidation messages Issue: OPFS write fails Solution: - Check storage quota: navigator.storage.estimate() - Request persistent storage: navigator.storage.persist() - Clear old caches to free space Issue: "SharedArrayBuffer is not defined" Solution: - Enable cross-origin isolation (COOP/COEP headers) - Check secure context (HTTPS/localhost) - Verify browser version supports SAB Manual OPFS Inspection: ------------------------ List OPFS files: ```javascript async function listOpfsFiles() { const root = await navigator.storage.getDirectory(); for await (const [name, handle] of root.entries()) { if (name.startsWith('mailbox-snapshot')) { const file = await handle.getFile(); console.log(`${name}: ${file.size} bytes`); } } } listOpfsFiles(); ``` Delete specific OPFS file: ```javascript async function deleteOpfsFile(filename) { const root = await navigator.storage.getDirectory(); await root.removeEntry(filename); console.log(`Deleted: ${filename}`); } deleteOpfsFile('mailbox-snapshot-abc123.sqlite3'); ``` Clear all OPFS files: ```javascript async function clearAllOpfs() { const root = await navigator.storage.getDirectory(); for await (const [name, handle] of root.entries()) { await root.removeEntry(name); console.log(`Deleted: ${name}`); } } clearAllOpfs(); ``` """ assert len(documentation) > 1000, "Debugging documentation should be comprehensive"

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Dicklesworthstone/mcp_agent_mail'

If you have feedback or need assistance with the MCP directory API, please join our Discord server