/** * Browser <-> backend pair check. * * Drives the real frontend served by the real backend (scripted runtime peer) * in a real Chrome via CDP. Usage: * node tests/integration/browser_backend.mjs --app http://127.0.0.1:8765 --control /path/to/control --chrome 9444 */ import assert from 'node:assert/strict'; import { writeFileSync } from 'node:fs'; const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, arr) => { if (a.startsWith('--')) acc.push([a.slice(2), arr[i + 1]]); return acc; }, [])); const APP = (args.app || 'http://127.0.0.1:8765').replace(/\/$/, ''); const CONTROL = args.control; const CHROME_PORT = Number(args.chrome || 9444); const setScenario = (name) => writeFileSync(CONTROL, name + '\n'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); class CDP { constructor(ws) { this.wsUrl = ws; this.id = 1; this.pending = new Map(); this.requests = []; this.responses = []; } async connect() { this.ws = new WebSocket(this.wsUrl); await new Promise((res, rej) => { this.ws.onopen = res; this.ws.onerror = rej; }); this.ws.onmessage = (e) => { const d = JSON.parse(e.data); if (d.id && this.pending.has(d.id)) { const p = this.pending.get(d.id); this.pending.delete(d.id); d.error ? p.reject(new Error(JSON.stringify(d.error))) : p.resolve(d.result); } else if (d.method === 'Network.requestWillBeSent') this.requests.push(d.params.request); else if (d.method === 'Network.responseReceived') this.responses.push({ url: d.params.response.url, status: d.params.response.status }); }; await this.send('Runtime.enable'); await this.send('Network.enable'); await this.send('Page.enable'); } send(method, params = {}) { return new Promise((resolve, reject) => { const id = this.id++; this.pending.set(id, { resolve, reject }); this.ws.send(JSON.stringify({ id, method, params })); }); } async eval(expression) { const r = await this.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) throw new Error(`eval: ${r.exceptionDetails.text} ${r.exceptionDetails.exception?.description || ''}`); return r.result?.value; } async navigate(url) { await this.send('Page.navigate', { url }); await sleep(900); } async waitFor(expression, timeoutMs = 15000, label = expression) { const t0 = Date.now(); while (Date.now() - t0 < timeoutMs) { if (await this.eval(expression)) return; await sleep(100); } const fb = await this.eval('(document.getElementById("modal-feedback")||{}).textContent + " | " + (document.getElementById("prompt-error")||{}).textContent'); throw new Error(`timeout waiting for ${label}; feedback=${JSON.stringify(fb)}; responses=${JSON.stringify(this.responses.slice(-6))}`); } close() { this.ws?.close(); } } const visible = (id) => `!document.getElementById("${id}").classList.contains("hidden")`; async function main() { const tabs = await (await fetch(`http://127.0.0.1:${CHROME_PORT}/json`)).json(); const tab = tabs.find((t) => t.type === 'page') || tabs[0]; if (!tab?.webSocketDebuggerUrl) throw new Error('no Chrome page tab'); const cdp = new CDP(tab.webSocketDebuggerUrl); await cdp.connect(); let passed = 0, failed = 0; const step = async (name, fn) => { process.stdout.write(`• ${name}... `); try { await fn(); console.log('PASS'); passed++; } catch (e) { console.log(`FAIL: ${e.message}`); failed++; } }; try { setScenario('standard'); await cdp.send('Network.clearBrowserCookies'); await cdp.navigate(APP + '/'); await step('Bootstrap: real index, HttpOnly cookie, headers', async () => { assert.equal(await cdp.eval('document.title'), 'Confluence Research'); assert.ok(await cdp.eval(visible('view-prompt'))); const { cookies } = await cdp.send('Network.getCookies', { urls: [APP + '/'] }); const c = cookies.find((k) => k.name === 'cw_session'); assert.ok(c, 'cw_session cookie must be set by GET /'); assert.equal(c.httpOnly, true); assert.equal(c.sameSite, 'Strict'); assert.equal(c.path, '/'); assert.ok(!(await cdp.eval('document.cookie')).includes('cw_session'), 'cookie must not be script-readable'); // Assets loaded from the same origin with a real stylesheet applied. const bg = await cdp.eval('getComputedStyle(document.body).backgroundColor'); assert.notEqual(bg, '', 'stylesheet must load'); assert.ok(await cdp.eval('typeof marked !== "undefined" && typeof DOMPurify !== "undefined"'), 'vendored libs must load'); }); await step('Verify credentials against the backend (403 then 200)', async () => { await cdp.eval('document.getElementById("key-btn").click()'); await cdp.eval('(() => { for (const [id, v] of [["cred-url", "https://approved.example.com"], ["cred-pat", "wrong-pat"]]) { const el = document.getElementById(id); el.value = v; el.dispatchEvent(new Event("input", { bubbles: true })); } })()'); await cdp.eval('document.getElementById("btn-test-cred").click()'); await cdp.waitFor('document.getElementById("modal-feedback").textContent.length > 0', 5000, 'verify feedback'); const bad = await cdp.eval('document.getElementById("modal-feedback").textContent'); assert.ok(!/successful/i.test(bad), `wrong PAT must not verify: ${bad}`); // Retype the token as a user would: the input event aborts any stale verify and re-enables the button. await cdp.eval('(() => { const p = document.getElementById("cred-pat"); p.value = "dev-pat"; p.dispatchEvent(new Event("input", { bubbles: true })); })()'); await cdp.eval('document.getElementById("btn-test-cred").click()'); await cdp.waitFor('/successful/i.test(document.getElementById("modal-feedback").textContent)', 5000, 'success feedback'); await cdp.eval('document.getElementById("btn-save-cred").click()'); await sleep(200); assert.ok(await cdp.eval('document.getElementById("cred-indicator").classList.contains("active")')); assert.ok(!(await cdp.eval('JSON.stringify(localStorage) + JSON.stringify(sessionStorage) + location.href')).includes('dev-pat')); }); await step('Query through the real backend: statuses, history, sources', async () => { await cdp.eval('document.getElementById("prompt-input").value = "How do I deploy service X?"'); await cdp.eval('document.getElementById("submit-btn").click()'); assert.ok(await cdp.eval(visible('view-loading')), 'loading view'); await cdp.waitFor(visible('view-result'), 20000, 'result view'); const text = await cdp.eval('document.getElementById("output-content").textContent'); assert.ok(text.includes('Deployment Guide'), text.slice(0, 200)); const link = await cdp.eval('(() => { const a = document.querySelector("#output-content a"); return a && { target: a.target, rel: a.rel, href: a.href }; })()'); assert.deepEqual(link, { target: '_blank', rel: 'noopener noreferrer', href: 'https://approved.example.com/pages/viewpage.action?pageId=847291' }); assert.ok((await cdp.eval('document.getElementById("history-toggle-title").textContent')).includes('1 page accessed')); await cdp.eval('document.getElementById("history-toggle-btn").click()'); assert.equal(await cdp.eval('document.querySelectorAll(".tool-card").length'), 2); assert.equal(await cdp.eval('document.querySelector(".page-title-link").textContent'), 'Deployment Guide'); const q = cdp.requests.filter((r) => r.url.endsWith('/api/v1/query')); assert.ok(q.length >= 1 && q[q.length - 1].method === 'POST'); // The backend enforces a matching Origin on mutations; a 200 proves the browser sent it. const qr = cdp.responses.filter((r) => r.url.endsWith('/api/v1/query')); assert.equal(qr[qr.length - 1].status, 200); }); await step('Artifact download: exact bytes, attachment headers, ownership', async () => { assert.equal(await cdp.eval('document.querySelector(".artifact-name").textContent'), 'checklist.md'); cdp.requests.length = 0; await cdp.eval('document.querySelector(".download-btn").click()'); await sleep(600); assert.ok(await cdp.eval('document.getElementById("artifacts-error").classList.contains("hidden")'), 'download error box hidden'); const dl = cdp.requests.find((r) => r.url.includes('/api/v1/artifacts/')); assert.ok(dl, 'download request observed'); const check = await cdp.eval(`fetch(${JSON.stringify(dl.url)}).then(async r => ({ status: r.status, ct: r.headers.get('content-type'), cd: r.headers.get('content-disposition'), xcto: r.headers.get('x-content-type-options'), cc: r.headers.get('cache-control'), body: new TextDecoder().decode(await r.arrayBuffer()) }))`); assert.equal(check.status, 200); assert.equal(check.body, '# Checklist\n\n- Deploy service X\n'); assert.ok(check.ct.startsWith('application/octet-stream')); assert.ok(check.cd.startsWith('attachment')); assert.equal(check.xcto, 'nosniff'); assert.equal(check.cc, 'no-store'); const unknown = await cdp.eval(`fetch('/api/v1/artifacts/not-a-real-id').then(async r => ({ status: r.status, body: await r.text() }))`); assert.equal(unknown.status, 404); assert.equal(JSON.parse(unknown.body).error.code, 'artifact_not_found'); // Another browser session (fresh cookie jar) gets the same 404. const other = await cdp.eval(`fetch(${JSON.stringify(dl.url)}, { credentials: 'omit' }).then(async r => ({ status: r.status, body: await r.text() }))`); assert.equal(other.status, 404); assert.equal(other.body, unknown.body); }); await step('Cancel during run: server observes abort, next query succeeds', async () => { await cdp.eval('document.getElementById("back-btn").click()'); setScenario('timeout'); await cdp.eval('document.getElementById("submit-btn").click()'); await sleep(400); assert.ok(await cdp.eval(visible('view-loading'))); await cdp.eval('document.getElementById("cancel-btn").click()'); await cdp.waitFor(visible('view-prompt'), 5000, 'prompt after cancel'); assert.equal(await cdp.eval('document.getElementById("prompt-input").value'), 'How do I deploy service X?'); setScenario('standard'); // Retry until the busy gate releases after cleanup. let ok = false; for (let i = 0; i < 20 && !ok; i++) { await cdp.eval('document.getElementById("submit-btn").click()'); await sleep(500); ok = await cdp.eval(visible('view-result')); if (!ok) { const err = await cdp.eval('document.getElementById("prompt-error").textContent'); assert.ok(/busy/i.test(err) || err === '', `unexpected error: ${err}`); } } assert.ok(ok, 'query after cancellation must succeed'); await cdp.eval('document.getElementById("back-btn").click()'); }); await step('Execution failure shows sanitized error and recovers', async () => { setScenario('agent_error'); await cdp.eval('document.getElementById("submit-btn").click()'); await cdp.waitFor('document.getElementById("prompt-error").textContent.length > 0', 10000, 'error shown'); const err = await cdp.eval('document.getElementById("prompt-error").textContent'); assert.ok(!/traceback|exception|stack/i.test(err), err); assert.ok(await cdp.eval(visible('view-prompt'))); setScenario('standard'); }); await step('Malicious Markdown/history under real headers: no script, no external requests', async () => { setScenario('malicious'); cdp.requests.length = 0; await cdp.eval('document.getElementById("submit-btn").click()'); await cdp.waitFor(visible('view-result'), 20000, 'result view'); await sleep(800); assert.equal(await cdp.eval('document.querySelectorAll("#output-content script, #output-content img, #output-content iframe, #output-content form").length'), 0); assert.equal(await cdp.eval('window.__xss'), undefined); const hrefs = await cdp.eval('Array.from(document.querySelectorAll("#output-content a")).map(a => a.getAttribute("href"))'); assert.ok(hrefs.every((h) => !/^javascript:/i.test(h || '')), JSON.stringify(hrefs)); assert.ok(hrefs.some((h) => h && h.startsWith('https://approved.example.com')), 'citation kept'); await cdp.eval('document.getElementById("history-toggle-btn").click()'); await sleep(200); assert.equal(await cdp.eval('document.querySelectorAll("#view-result script").length'), 0); const external = cdp.requests.filter((r) => !r.url.startsWith(APP + '/') && !r.url.startsWith('data:') && !r.url.startsWith('blob:')); assert.deepEqual(external.map((r) => r.url), [], 'no automatic external requests'); setScenario('standard'); await cdp.eval('document.getElementById("back-btn").click()'); }); await step('Clear credentials blocks queries', async () => { await cdp.eval('document.getElementById("key-btn").click()'); const clearBtn = await cdp.eval('!!document.getElementById("btn-clear-cred")'); if (clearBtn) { await cdp.eval('document.getElementById("btn-clear-cred").click()'); await sleep(200); assert.ok(!(await cdp.eval('document.getElementById("cred-indicator").classList.contains("active")'))); } else { await cdp.eval('document.getElementById("btn-cancel-cred").click()'); } }); } finally { cdp.close(); } console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0); } main().catch((e) => { console.error(e); process.exit(2); });