import { describe, it, expect, vi } from 'vitest'; import { JSDOM } from 'jsdom'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const html = readFileSync(fileURLToPath(new URL('../public/index.html', import.meta.url)), 'utf8'); const setupPage = readFileSync(fileURLToPath(new URL('../public/mcp-setup.html', import.meta.url)), 'utf8'); const composeLoader = readFileSync(fileURLToPath(new URL('../public/compose-index.html', import.meta.url)), 'utf8'); const deployScript = readFileSync(fileURLToPath(new URL('../scripts/gb-auto-deploy.sh', import.meta.url)), 'utf8'); const script = readFileSync(fileURLToPath(new URL('../public/js/mcp-setup.js', import.meta.url)), 'utf8'); describe('MCP setup in Settings', () => { it('uses the current browser host and copies working client templates without a secret', async () => { const dom = new JSDOM(html, { url: 'https://ghost-browser.example.test/', runScripts: 'outside-only', }); const copied = []; Object.defineProperty(dom.window.navigator, 'clipboard', { value: { writeText: vi.fn(async value => copied.push(value)) }, }); dom.window.eval(script); const doc = dom.window.document; expect(doc.querySelector('#mcpSetup')).not.toBeNull(); expect(doc.querySelector('#mcpEndpoint').value).toBe('https://ghost-browser.example.test/mcp'); expect(doc.querySelector('#mcpCodexConfig').textContent).toContain('bearer_token_env_var = "GHOST_BROWSER_KEY"'); expect(doc.querySelector('#mcpClaudeCommand').textContent).toContain('--header "Authorization: Bearer YOUR_GHOST_BROWSER_KEY"'); expect(doc.querySelector('#mcpClaudeCommand').textContent).toContain('--scope user'); expect(html).toContain(''); doc.querySelector('#mcpCopyUrl').click(); doc.querySelector('#mcpCopyCodex').click(); doc.querySelector('#mcpCopyClaude').click(); await vi.waitFor(() => expect(copied).toHaveLength(3)); expect(copied[0]).toBe('https://ghost-browser.example.test/mcp'); expect(copied[1]).toBe(doc.querySelector('#mcpCodexConfig').textContent); expect(copied[2]).toBe(doc.querySelector('#mcpClaudeCommand').textContent); await vi.waitFor(() => expect(doc.querySelector('#mcpSetupMsg').textContent).toContain('copied')); dom.window.close(); }); it('reports clipboard failures without exposing a key', async () => { const dom = new JSDOM(html, { url: 'https://example.test/', runScripts: 'outside-only' }); Object.defineProperty(dom.window.navigator, 'clipboard', { value: { writeText: vi.fn(async () => { throw new Error('denied'); }) }, }); dom.window.eval(script); dom.window.document.querySelector('#mcpCopyUrl').click(); await vi.waitFor(() => expect(dom.window.document.querySelector('#mcpSetupMsg').textContent).toContain('Could not copy')); dom.window.close(); }); it('offers setup in the deployed Compose console and reveals a key only on request', async () => { expect(composeLoader).not.toContain('mcp-link'); expect(composeLoader).not.toContain('href="/mcp-setup.html"'); const shared = readFileSync(fileURLToPath(new URL('../mobile/shared/src/commonMain/kotlin/engineer/myapp/gb/shared/SettingsShared.kt', import.meta.url)), 'utf8'); const web = readFileSync(fileURLToPath(new URL('../mobile/shared/src/wasmJsMain/kotlin/engineer/myapp/gb/shared/Main.kt', import.meta.url)), 'utf8'); expect(shared.match(/SGhost\("MCP setup"/g)).toHaveLength(2); expect(web).toContain('onOpenMcpSetup = { openTab("/mcp-setup.html") }'); const docker = readFileSync(fileURLToPath(new URL('../Dockerfile', import.meta.url)), 'utf8'); expect(docker).toContain(':shared:wasmJsBrowserDistribution'); expect(docker).toContain('COPY --from=console-build'); expect(composeLoader).toContain('id="ComposeTarget"'); expect(composeLoader).toContain('src="/js/console-auth.js"'); expect(deployScript).toContain('LOADER_SRC="$SRC/public/compose-index.html"'); const dom = new JSDOM(setupPage, { url: 'https://ghost-browser.example.test/mcp-setup.html', runScripts: 'outside-only' }); const fetchKey = vi.fn(async () => ({ ok: true, json: async () => ({ keys: [{ key: 'test-secret' }] }) })); dom.window.fetch = fetchKey; dom.window.eval(script); expect(fetchKey).not.toHaveBeenCalled(); dom.window.document.querySelector('#mcpRevealKey').click(); await vi.waitFor(() => expect(dom.window.document.querySelector('#mcpKeyBox').textContent).toBe('test-secret')); expect(fetchKey).toHaveBeenCalledWith('/api/auth/key', { credentials: 'same-origin' }); dom.window.close(); }); });