/* * LinkdingSync Test Orchestrator (Inline Version) * Self-contained - paste entire file directly into Firefox DevTools Console * * Instructions: * 1. Open Firefox DevTools → Console tab * 2. Copy ENTIRE file contents * 3. Paste into console (Ctrl+Shift+V) * 4. Wait for "LinkdingSync Test Suite loaded" * 5. Run: runAllTestsWithReset() */ 'use strict'; (function() { // ==================================================================== // CONFIGURATION // ==================================================================== const CONFIG = { serverUrl: 'https://links.blabber1565.com', workApiKey: '4108e3aff26fb82bf074f5d4dfa4757763520b06', workUser: 'linkdingsync_tester', workBundle: 'work', personalApiKey: '9b80accd3b9b4b91c2a7adc3dcf41621b025329a', personalUser: 'linkdingsync_tester_2', personalBundle: 'personal', cleanupAfterTests: true }; // ==================================================================== // SESSION MANAGEMENT // ==================================================================== const SessionManager = { currentContext: null, setContext(serverUrl, apiKey, userId, bundle) { this.currentContext = { serverUrl: serverUrl.endsWith('/') ? serverUrl : serverUrl + '/', apiKey, userId, bundle }; return this; }, getHeaders() { if (!this.currentContext) { throw new Error('No context set. Call setContext() first.'); } return { 'Authorization': `Token ${this.currentContext.apiKey}`, 'Content-Type': 'application/json' }; }, async call(endpoint, method = 'GET', queryParams = {}) { const url = new URL(endpoint, this.currentContext.serverUrl); Object.entries(queryParams).forEach(([key, value]) => { url.searchParams.append(key, value); }); const response = await fetch(url, { method, headers: this.getHeaders(), body: null }); if (!response.ok) { const text = await response.text().slice(0, 200); throw new Error(`${response.status}: ${response.statusText} - ${text}`); } return await response.json(); } }; // ==================================================================== // HELPERS // ==================================================================== const Helpers = { generateTestId(prefix = 'test') { return `${prefix}-${Date.now().toString().slice(-4)}-${Math.random().toString(36).substring(2, 4)}`; }, async createBookmark(url, options = {}) { const testId = this.generateTestId(); const baseUrl = new URL(url); baseUrl.hostname = `${testId}.${baseUrl.hostname}`; const bookmarkData = { url: baseUrl.href, title: options.title || `Test: ${testId}`, description: options.description || 'Test bookmark', notes: JSON.stringify({ path: options.path || `Test/${testId}`, userNotes: options.notes || 'Test bookmark', testId }) }; const response = await SessionManager.call('/api/bookmarks/', 'POST', null, {}); console.log(` Created: ID=${response.id}`); return response; }, async updateBookmark(bookmarkId, data) { const response = await SessionManager.call(`/api/bookmarks/${bookmarkId}/`, 'PUT', null, {}); console.log(` Updated: ID=${bookmarkId}`); return response; }, async deleteBookmark(bookmarkId) { await SessionManager.call(`/api/bookmarks/${bookmarkId}/`, 'DELETE', {}); console.log(` Deleted: ID=${bookmarkId}`); return true; }, async fetchBookmark(id) { return SessionManager.call(`/api/bookmarks/${id}/`); }, parseNotes(noteString) { if (!noteString) return null; try { const parsed = JSON.parse(noteString); return parsed; } catch { return { userNotes: noteString, version: '1.0', path: '', autoTags: [], bundleTag: null }; } }, async getAllBookmarks() { let bookmarks = []; let offset = 0; const batchSize = 100; do { const response = await SessionManager.call('/api/bookmarks/', 'GET', { limit: batchSize, offset }); bookmarks.push(...(response.results || [])); offset += batchSize; } while (bookmarks.length > offset); return bookmarks; }, async resetBookmarks() { console.log('[Utils] Resetting all bookmarks...'); try { const allBookmarks = await this.getAllBookmarks(); const testBookmarks = allBookmarks.filter(b => b.testId); if (testBookmarks.length > 0) { console.log(`[Utils] Found ${testBookmarks.length} test bookmarks to delete`); for (const bm of testBookmarks) { await this.deleteBookmark(bm.id); } console.log('[Utils] Reset complete'); } else { console.log('[Utils] No test bookmarks found'); } } catch (error) { console.error('[Utils] Reset failed:', error.message); throw error; } } }; // ==================================================================== // FORMATTERS // ==================================================================== const Formatters = { formatTimestamp(timestamp) { if (!timestamp) return 'Never'; return new Date(timestamp).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }, consoleHeader(text) { console.log(''.padEnd(60, '=')); console.log(` ${text}`.padEnd(60, '='.charCodeAt(0) === text.charCodeAt(0) ? '=' : '-').padEnd(60, '=')); console.log(''.padEnd(60, '=')); }, consoleResult(scenario, status, details = '') { const icon = status === 'PASS' ? '✓' : status === 'FAIL' ? '✗' : '⚠'; const emoji = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⚠️'; console.log(` [${scenario}] ${icon} ${emoji} ${status}`); if (details) console.log(` ${details}`); } }; // ==================================================================== // TEST MODULE: ISOLATION // ==================================================================== async function test1_SameUserDifferentKeys() { console.log('\n=== Test 1: Same URL, Different API Keys, Same User ==='); console.log('Purpose: Verify if API keys provide isolation within same user'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const bm1 = await Helpers.createBookmark('https://isolation-test.example.com', { title: 'Isolation Test - Work Key' }); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const bm2 = await Helpers.createBookmark('https://isolation-test.example.com', { title: 'Isolation Test - Personal Key' }); console.log(` Work bookmark ID: ${bm1.id}`); console.log(` Personal bookmark ID: ${bm2.id}`); if (bm1.id === bm2.id) { Formatters.consoleResult('Test 1', 'FAIL', 'Same bookmark ID - API keys do NOT provide isolation'); console.log(' → Same user means same bookmarks regardless of API key'); return { pass: false, reason: 'API keys do not provide isolation within same user' }; } else { Formatters.consoleResult('Test 1', 'PASS', 'Different bookmark IDs - API keys provide isolation'); console.log(' → Different API keys create separate bookmarks'); return { pass: true, ids: { work: bm1.id, personal: bm2.id } }; } } catch (error) { Formatters.consoleResult('Test 1', 'FAIL', error.message); throw error; } } async function test2_DifferentUsers() { console.log('\n=== Test 2: Different Users - Verify Isolation ==='); console.log('Purpose: Verify isolation between different users'); try { const workUrl = 'https://cross-user-isolation.example.com'; SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const workBookmark = await Helpers.createBookmark(workUrl, { title: 'Cross-User Test - Work' }); console.log(` Bookmark created by work user: ID=${workBookmark.id}`); const workFetch = await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`); console.log(` Work user sees bookmark: ${workFetch.title}`); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalFetch = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); console.log(` Personal user sees ${personalFetch.count || personalFetch.results?.length || 0} bookmarks`); if (personalFetch.results && personalFetch.results.length > 0) { Formatters.consoleResult('Test 2', 'FAIL', 'Users can see each other\'s bookmarks'); console.log(' → Sharing enabled or same underlying user'); return { pass: false, reason: 'Users can see each other\'s bookmarks' }; } else { Formatters.consoleResult('Test 2', 'PASS', 'Proper user isolation exists'); console.log(' → Can use different API keys for isolation'); return { pass: true }; } } catch (error) { Formatters.consoleResult('Test 2', 'FAIL', error.message); throw error; } } async function runIsolationTests() { console.log('\n' + '='.repeat(60)); console.log(' API Key & User Isolation Tests'); console.log('='.repeat(60)); const results = []; try { results[0] = await test1_SameUserDifferentKeys(); results[1] = await test2_DifferentUsers(); } catch (error) { console.error('Test suite error:', error.message); await Helpers.resetBookmarks(); } console.log('\n' + '='.repeat(60)); console.log(' Isolation Tests Complete'); console.log('='.repeat(60)); return results; } // ==================================================================== // TEST MODULE: CONFLICTS // ==================================================================== async function test3_ConflictResolution() { console.log('\n=== Test 3: Conflict Resolution - Different Paths ==='); console.log('Purpose: How server handles same URL in different paths with different API keys'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const workUrl = 'https://conflict-resolution.example.com'; const workBookmark = await Helpers.createBookmark(workUrl, { title: 'Conflict Resolution Test', path: 'Work/Development', notes: 'Work Development Notes' }); console.log(` Work bookmark ID: ${workBookmark.id}`); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalBookmark = await Helpers.createBookmark(workUrl, { title: 'Conflict Resolution Test', path: 'Personal/Notes', notes: 'Personal Notes' }); console.log(` Personal bookmark ID: ${personalBookmark.id}`); console.log(`\nComparing bookmark IDs: work=${workBookmark.id}, personal=${personalBookmark.id}`); if (workBookmark.id === personalBookmark.id) { Formatters.consoleResult('Test 3', 'FAIL', 'Same bookmark ID'); console.log(' → Server merges bookmarks by URL'); console.log(' → Need path merge strategy'); const state = await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`); const parsed = Helpers.parseNotes(state.notes); console.log(` → Current path: ${parsed.path}`); console.log(` → Current notes: ${parsed.userNotes}`); return { pass: false, sameId: true, path: parsed.path }; } else { Formatters.consoleResult('Test 3', 'PASS', 'Different bookmark IDs'); console.log(' → Server creates separate bookmarks per API key'); console.log(' → Can use different API keys for isolation'); return { pass: true, sameId: false, workId: workBookmark.id, personalId: personalBookmark.id }; } } catch (error) { Formatters.consoleResult('Test 3', 'FAIL', error.message); throw error; } } async function test4_TitleDescriptionConflict() { console.log('\n=== Test 4: Title/Description Conflict ==='); console.log('Purpose: How server resolves conflicts for title/description fields'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const testUrl = 'https://title-conflict.example.com'; const bookmark = await Helpers.createBookmark(testUrl, { title: 'Initial Title', description: 'Initial Description', path: 'Initial' }); await Helpers.updateBookmark(bookmark.id, { title: 'Work Title', description: 'Work Description', notes: JSON.stringify({ path: 'Work/Dev', userNotes: 'Work notes', autoTags: [{name: 'Work'}] }) }); console.log(' Updated via Work: Work Title'); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); await Helpers.updateBookmark(bookmark.id, { title: 'Personal Title', description: 'Personal Description', notes: JSON.stringify({ path: 'Personal/Notes', userNotes: 'Personal notes', autoTags: [{name: 'Personal'}] }) }); console.log(' Updated via Personal: Personal Title'); const final = await SessionManager.call(`/api/bookmarks/${bookmark.id}/`); const parsed = Helpers.parseNotes(final.notes); console.log('\nFinal state:'); console.log(` Title: ${final.title}`); console.log(` Description: ${final.description}`); console.log(` Path: ${parsed.path}`); console.log(` User notes: ${parsed.userNotes}`); if (final.title === 'Personal Title') { Formatters.consoleResult('Test 4', 'PASS', 'Last-write-wins (Personal took precedence)'); return { pass: true, strategy: 'last-write-wins', winner: 'personal' }; } else if (final.title === 'Work Title') { Formatters.consoleResult('Test 4', 'PASS', 'Last-write-wins (Work took precedence)'); return { pass: true, strategy: 'last-write-wins', winner: 'work' }; } else if (final.title.includes('Work') && final.title.includes('Personal')) { Formatters.consoleResult('Test 4', 'PASS', 'Merged title'); return { pass: true, strategy: 'merge', winner: 'merged' }; } else { Formatters.consoleResult('Test 4', 'WARN', 'Unexpected title value'); return { pass: null, strategy: 'unknown', winner: final.title }; } } catch (error) { Formatters.consoleResult('Test 4', 'FAIL', error.message); throw error; } } async function runConflictTests() { console.log('\n' + '='.repeat(60)); console.log(' Conflict Resolution Tests'); console.log('='.repeat(60)); const results = []; try { results[0] = await test3_ConflictResolution(); results[1] = await test4_TitleDescriptionConflict(); } catch (error) { console.error('Test suite error:', error.message); await Helpers.resetBookmarks(); } console.log('\n' + '='.repeat(60)); console.log(' Conflict Tests Complete'); console.log('='.repeat(60)); return results; } // ==================================================================== // TEST MODULE: DELETION // ==================================================================== async function test5_DeletePropagation() { console.log('\n=== Test 5: Delete Propagation ==='); console.log('Purpose: Confirm if deleting affects all API keys'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const testUrl = 'https://delete-propagation.example.com'; const workBookmark = await Helpers.createBookmark(testUrl, { title: 'Delete Prop Test', path: 'Work/Dev' }); console.log(` Work bookmark ID: ${workBookmark.id}`); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalBookmark = await Helpers.createBookmark(testUrl, { title: 'Delete Prop Test', path: 'Personal/Notes' }); console.log(` Personal bookmark ID: ${personalBookmark.id}`); console.log(` Same bookmark? ${workBookmark.id === personalBookmark.id}`); await SessionManager.call(`/api/bookmarks/${workBookmark.id}/`, 'DELETE', {}); console.log(' Deleted via Work key'); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const deleted = personalList.results?.find(b => b.url === testUrl); if (!deleted) { Formatters.consoleResult('Test 5', 'FAIL', 'Delete propagated (same bookmark)'); console.log(' → Deleting via one key deletes all'); return { pass: false, propagated: true, sameBookmark: true }; } else { Formatters.consoleResult('Test 5', 'PASS', 'Delete did not propagate (separate bookmarks)'); console.log(' → Each bookmark exists independently'); console.log(' → Can delete via specific API key'); return { pass: true, propagated: false, sameBookmark: false }; } } catch (error) { Formatters.consoleResult('Test 5', 'FAIL', error.message); throw error; } } async function test6_DeleteSameUserDifferentKeys() { console.log('\n=== Test 6: Delete - Same User, Different Keys ==='); console.log('Purpose: Verify delete behavior when same user, different API keys'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const testUrl = 'https://delete-same-user.example.com'; const bm1 = await Helpers.createBookmark(testUrl, { title: 'Same User Delete Test - Key 1' }); console.log(` Created with Key 1: ID=${bm1.id}`); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const bm2 = await Helpers.createBookmark(testUrl, { title: 'Same User Delete Test - Key 2' }); console.log(` Created with Key 2: ID=${bm2.id}`); if (bm1.id === bm2.id) { Formatters.consoleResult('Test 6', 'WARN', 'Same bookmark - delete propagates'); console.log(' → Same user means same bookmark'); console.log(' → Deleting via either key removes it'); await Helpers.deleteBookmark(bm1.id); const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const exists = workList.results?.find(b => b.url === testUrl); if (!exists) { console.log(' → Verified: bookmark deleted via both keys'); } return { pass: true, same: true, propagates: true }; } else { Formatters.consoleResult('Test 6', 'PASS', 'Different bookmarks - delete is isolated'); console.log(' → Different API keys create different bookmarks'); console.log(' → Can delete independently'); await Helpers.deleteBookmark(bm1.id); const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const workGone = !workList.results?.find(b => b.url === testUrl); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const personalExists = personalList.results?.find(b => b.url === testUrl); if (workGone && personalExists) { console.log(' → Verified: work deleted, personal still exists'); } return { pass: true, same: false, propagates: false }; } } catch (error) { Formatters.consoleResult('Test 6', 'FAIL', error.message); throw error; } } async function runDeletionTests() { console.log('\n' + '='.repeat(60)); console.log(' Deletion Tests'); console.log('='.repeat(60)); const results = []; try { results[0] = await test5_DeletePropagation(); results[1] = await test6_DeleteSameUserDifferentKeys(); } catch (error) { console.error('Test suite error:', error.message); await Helpers.resetBookmarks(); } console.log('\n' + '='.repeat(60)); console.log(' Deletion Tests Complete'); console.log('='.repeat(60)); return results; } // ==================================================================== // TEST MODULE: BUNDLES // ==================================================================== async function test7_BundleTagFiltering() { console.log('\n=== Test 7: Bundle Tag Filtering ==='); console.log('Purpose: Verify if bundle tags filter bookmarks properly'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const testUrl = 'https://bundle-filter.example.com'; const bookmark = await Helpers.createBookmark(testUrl, { title: 'Bundle Filter Test', path: 'Test/Path', notes: 'Work bundle tag' }); console.log(` Bookmark created: ID=${bookmark.id}`); const workBundleResponse = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); console.log(` Work bundle has ${workBundleResponse.results?.filter(b => b.testId).length || 0} test bookmarks`); if (workBundleResponse.results?.filter(b => b.testId).length > 0) { Formatters.consoleResult('Test 7', 'PASS', 'Bundle tags filter bookmarks'); console.log(' → Work bundle has work-tagged bookmarks'); return { pass: true, filtered: true }; } else { Formatters.consoleResult('Test 7', 'WARN', 'Bundle filtering unclear'); console.log(' → May need to use tags for filtering'); return { pass: null, filtered: null }; } } catch (error) { Formatters.consoleResult('Test 7', 'FAIL', error.message); throw error; } } async function test8_BundleSpecificSync() { console.log('\n=== Test 8: Bundle-Specific Sync ==='); console.log('Purpose: Verify sync behavior with different bundles'); try { SessionManager.setContext(CONFIG.serverUrl, CONFIG.workApiKey, CONFIG.workUser, CONFIG.workBundle); const workUrl = 'https://bundle-specific-work.example.com'; const workBookmark = await Helpers.createBookmark(workUrl, { title: 'Bundle Specific - Work', path: 'Work/Bundle', notes: 'Work bundle content' }); console.log(` Work bookmark: ID=${workBookmark.id}`); SessionManager.setContext(CONFIG.serverUrl, CONFIG.personalApiKey, CONFIG.personalUser, CONFIG.personalBundle); const personalUrl = 'https://bundle-specific-personal.example.com'; const personalBookmark = await Helpers.createBookmark(personalUrl, { title: 'Bundle Specific - Personal', path: 'Personal/Bundle', notes: 'Personal bundle content' }); console.log(` Personal bookmark: ID=${personalBookmark.id}`); const workList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const personalList = await SessionManager.call('/api/bookmarks/', 'GET', { limit: 100 }); const workCount = workList.results?.filter(b => b.testId).length || 0; const personalCount = personalList.results?.filter(b => b.testId).length || 0; console.log(` Work has ${workCount} test bookmarks`); console.log(` Personal has ${personalCount} test bookmarks`); if (workCount === 1 && personalCount === 1) { Formatters.consoleResult('Test 8', 'PASS', 'Bundles provide logical separation'); console.log(' → Can maintain separate sync for each bundle'); return { pass: true, workCount, personalCount }; } else { Formatters.consoleResult('Test 8', 'WARN', 'Bundle counts differ from expected'); console.log(' → May need to use tags for proper isolation'); return { pass: null, workCount, personalCount }; } } catch (error) { Formatters.consoleResult('Test 8', 'FAIL', error.message); throw error; } } async function runBundleTests() { console.log('\n' + '='.repeat(60)); console.log(' Bundle Tests'); console.log('='.repeat(60)); const results = []; try { results[0] = await test7_BundleTagFiltering(); results[1] = await test8_BundleSpecificSync(); } catch (error) { console.error('Test suite error:', error.message); await Helpers.resetBookmarks(); } console.log('\n' + '='.repeat(60)); console.log(' Bundle Tests Complete'); console.log('='.repeat(60)); return results; } // ==================================================================== // MAIN ORCHESTRATOR // ==================================================================== async function runAllTests() { console.log(''.padEnd(60, '=')); console.log(' LINKDINGSYNC - Complete Test Suite'); console.log('='.repeat(60)); console.log(''); const results = []; try { results = await runIsolationTests(); results = results.concat(await runConflictTests()); results = results.concat(await runDeletionTests()); results = results.concat(await runBundleTests()); } catch (error) { console.error('Test suite error:', error.message); console.log(''); console.log('[Orchestrator] Attempting cleanup...'); try { await Helpers.resetBookmarks(); } catch (cleanupError) { console.error('Cleanup failed:', cleanupError.message); } } // Summary const passed = results.filter(r => r.pass === true).length; const failed = results.filter(r => r.pass === false).length; const warnings = results.filter(r => r.pass === null || r.pass === undefined).length; Formatters.consoleHeader('Test Summary'); console.log(` Total: ${results.length}`); console.log(` Passed: ${passed}`); console.log(` Failed: ${failed}`); console.log(` Warning: ${warnings}`); console.log(''.padEnd(60, '=')); return results; } async function runAllTestsWithReset() { console.log(''.padEnd(60, '=')); console.log(' LINKDINGSYNC - Test Suite with Reset'); console.log('='.repeat(60)); try { await Helpers.resetBookmarks(); console.log('[Reset] Test bookmarks cleaned'); } catch (error) { console.error('[Reset] Failed:', error.message); } return await runAllTests(); } async function reset() { console.log('[Orchestrator] Resetting test bookmarks...'); await Helpers.resetBookmarks(); console.log('[Orchestrator] Reset complete'); } // ==================================================================== // EXPORT TO WINDOW // ==================================================================== window.LinkdingSyncTests = { CONFIG, SessionManager, Helpers, Formatters, runAllTests, runAllTestsWithReset, reset, runIsolationTests, runConflictTests, runDeletionTests, runBundleTests, test1_SameUserDifferentKeys, test2_DifferentUsers, test3_ConflictResolution, test4_TitleDescriptionConflict, test5_DeletePropagation, test6_DeleteSameUserDifferentKeys, test7_BundleTagFiltering, test8_BundleSpecificSync }; console.log(''); console.log('LinkdingSync Test Suite loaded successfully'); console.log(''); console.log('Commands:'); console.log(' runAllTests() - Run all tests'); console.log(' runAllTestsWithReset() - Run with cleanup first'); console.log(' reset() - Clean up test bookmarks'); console.log(' runModule("name") - Run specific module'); console.log(''); console.log('Test modules:'); console.log(' isolation - API key & user isolation (Tests 1-2)'); console.log(' conflicts - Conflict resolution (Tests 3-4)'); console.log(' deletion - Delete propagation (Tests 5-6)'); console.log(' bundles - Bundle filtering (Tests 7-8)'); console.log(''); })();