|
| 1 | +/** |
| 2 | + * E2E tests for Issue #12 (Windows path validation) and Issue #11 (moveMcp project scope) |
| 3 | + * Run: cd claude-code-organizer && DISPLAY=:0 node tests/pw-windows-fix.cjs |
| 4 | + */ |
| 5 | +const { chromium } = require('/home/nicole/.nvm/versions/node/v20.19.4/lib/node_modules/playwright'); |
| 6 | + |
| 7 | +(async () => { |
| 8 | + const browser = await chromium.launch({ headless: false }); |
| 9 | + const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); |
| 10 | + const errors = []; |
| 11 | + page.on('pageerror', e => errors.push(e.message)); |
| 12 | + let passed = 0, failed = 0, skipped = 0; |
| 13 | + |
| 14 | + function ok(name) { passed++; console.log(` ✅ ${name}`); } |
| 15 | + function fail(name, reason) { failed++; console.log(` ❌ ${name}: ${reason}`); } |
| 16 | + function skip(name, reason) { skipped++; console.log(` ⚠️ ${name}: ${reason}`); } |
| 17 | + |
| 18 | + try { |
| 19 | + await page.goto('http://localhost:3847'); |
| 20 | + await page.waitForTimeout(2000); |
| 21 | + |
| 22 | + // Get scan data via API (avoids UI timing issues) |
| 23 | + const scanData = await page.evaluate(() => fetch('/api/scan').then(r => r.json())); |
| 24 | + |
| 25 | + // ═══ TEST 1: file-content API works with absolute paths ═══ |
| 26 | + console.log('\nTEST 1: /api/file-content accepts absolute paths'); |
| 27 | + const fileItem = scanData.items?.find(i => i.path && i.category !== 'session'); |
| 28 | + if (fileItem) { |
| 29 | + const resp = await page.evaluate(async (p) => { |
| 30 | + const r = await fetch(`/api/file-content?path=${encodeURIComponent(p)}`); |
| 31 | + return r.json(); |
| 32 | + }, fileItem.path); |
| 33 | + if (resp.ok || resp.content !== undefined) ok('file-content returns data for: ' + fileItem.path.split('/').pop()); |
| 34 | + else if (resp.error?.includes('Invalid')) fail('file-content rejected valid path', resp.error); |
| 35 | + else ok('file-content responded (may be dir/binary): ' + (resp.error || '').slice(0, 50)); |
| 36 | + } else skip('file-content', 'no items with paths'); |
| 37 | + |
| 38 | + // ═══ TEST 2: export API accepts absolute path ═══ |
| 39 | + console.log('\nTEST 2: /api/export validates absolute paths'); |
| 40 | + const exportResp = await page.evaluate(async () => { |
| 41 | + return fetch('/api/export', { |
| 42 | + method: 'POST', |
| 43 | + headers: { 'Content-Type': 'application/json' }, |
| 44 | + body: JSON.stringify({ exportDir: '/tmp/cco-test-export' }), |
| 45 | + }).then(r => r.json()); |
| 46 | + }); |
| 47 | + if (exportResp.ok) ok('export accepted /tmp/cco-test-export'); |
| 48 | + else if (exportResp.error?.includes('Invalid')) fail('export rejected valid absolute path', exportResp.error); |
| 49 | + else ok('export responded: ' + (exportResp.error || exportResp.message || '').slice(0, 50)); |
| 50 | + |
| 51 | + // ═══ TEST 3: export API rejects relative path ═══ |
| 52 | + console.log('\nTEST 3: /api/export rejects relative paths'); |
| 53 | + const relResp = await page.evaluate(async () => { |
| 54 | + return fetch('/api/export', { |
| 55 | + method: 'POST', |
| 56 | + headers: { 'Content-Type': 'application/json' }, |
| 57 | + body: JSON.stringify({ exportDir: 'relative/path' }), |
| 58 | + }).then(r => r.json()); |
| 59 | + }); |
| 60 | + if (!relResp.ok) ok('export correctly rejected relative path'); |
| 61 | + else fail('export accepted relative path (should reject)', ''); |
| 62 | + |
| 63 | + // ═══ TEST 4: Scanner discovers .claude.json servers with projectKey ═══ |
| 64 | + console.log('\nTEST 4: Scanner includes claudeJsonProjectKey'); |
| 65 | + const mcpItems = scanData.items?.filter(i => i.category === 'mcp') || []; |
| 66 | + const claudeJsonItems = mcpItems.filter(i => i.fileName === '.claude.json'); |
| 67 | + const withProjectKey = claudeJsonItems.filter(i => i.claudeJsonProjectKey); |
| 68 | + |
| 69 | + console.log(` MCP items: ${mcpItems.length}, from .claude.json: ${claudeJsonItems.length}, with projectKey: ${withProjectKey.length}`); |
| 70 | + if (claudeJsonItems.length > 0) ok(`found ${claudeJsonItems.length} .claude.json servers`); |
| 71 | + else skip('claudeJson servers', 'no .claude.json MCP servers found'); |
| 72 | + |
| 73 | + if (withProjectKey.length > 0) { |
| 74 | + for (const item of withProjectKey) { |
| 75 | + console.log(` ${item.name} → projectKey: ${item.claudeJsonProjectKey.slice(-40)}`); |
| 76 | + } |
| 77 | + ok(`${withProjectKey.length} servers have claudeJsonProjectKey`); |
| 78 | + } else { |
| 79 | + skip('claudeJsonProjectKey', 'no project-scope servers in .claude.json (need `claude mcp add --scope project`)'); |
| 80 | + } |
| 81 | + |
| 82 | + // ═══ TEST 5: No duplicate MCP server from same file ═══ |
| 83 | + console.log('\nTEST 5: No duplicate MCP servers (same scope + same file)'); |
| 84 | + const seen = new Set(); |
| 85 | + let dupes = 0; |
| 86 | + for (const item of mcpItems) { |
| 87 | + // Dupes across DIFFERENT files (e.g. .mcp.json vs .claude.json) are user config issues, not bugs |
| 88 | + const key = `${item.scopeId}::${item.name}::${item.path}`; |
| 89 | + if (seen.has(key)) { dupes++; console.log(` DUPE: ${item.name} in ${item.scopeId} (${item.path})`); } |
| 90 | + seen.add(key); |
| 91 | + } |
| 92 | + if (dupes === 0) ok('no duplicates from same file'); |
| 93 | + else fail(`${dupes} duplicate MCP servers from same file`, ''); |
| 94 | + |
| 95 | + // ═══ TEST 6: No JS errors ═══ |
| 96 | + console.log('\nTEST 6: No JavaScript errors'); |
| 97 | + if (errors.length === 0) ok('zero JS errors'); |
| 98 | + else fail(`${errors.length} JS errors`, errors.join('; ')); |
| 99 | + |
| 100 | + // Summary |
| 101 | + await page.screenshot({ path: '/tmp/pw-windows-fix.png' }); |
| 102 | + console.log(`\n═══ RESULTS: ${passed} passed, ${failed} failed, ${skipped} skipped ═══`); |
| 103 | + if (failed > 0) process.exitCode = 1; |
| 104 | + } catch (e) { |
| 105 | + console.error('❌ FATAL:', e.message); |
| 106 | + await page.screenshot({ path: '/tmp/pw-windows-fix-err.png' }); |
| 107 | + process.exitCode = 1; |
| 108 | + } finally { |
| 109 | + await page.waitForTimeout(2000); |
| 110 | + await browser.close(); |
| 111 | + } |
| 112 | +})(); |
0 commit comments