#!/usr/bin/env node // Claude connector, end to end: starts this Theseus checkout on a throwaway // profile (THESEUS_USER_DATA, THESEUS_NO_UPDATE_CHECK) with the connector // turned on in that profile, serves a local test page, and drives it over // MCP the way Claude Code does: // // - type into a contenteditable editor (real insertText + Enter) // - paste_html with a heading and a link (trusted paste through the real // clipboard; the user's clipboard comes back afterwards) // - upload a PNG from the allowed folder; refuse one outside it // - refuse typing into a password field, by ref and by focus // - no token / a browser Origin is refused; evaluate is not offered // // Usage: node tests/claude-connector.test.mjs [--keep] [--shot out.png] // --keep leave Theseus running and the profile on disk afterwards import { spawn, execFileSync } from "node:child_process"; import { createRequire } from "node:module"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import http from "node:http"; import zlib from "node:zlib"; import crypto from "node:crypto"; import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const here = path.dirname(fileURLToPath(import.meta.url)); const appDir = path.resolve(here, ".."); const KEEP = process.argv.includes("--keep"); const SHOT = process.argv.includes("--shot") ? process.argv[process.argv.indexOf("--shot") + 1] : null; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); let passed = 0, failed = 0; function check(name, cond, detail) { if (cond) { passed++; console.log(` ok ${name}`); } else { failed++; console.log(` FAIL ${name}${detail ? `\n ${detail}` : ""}`); } } // ---- fixtures -------------------------------------------------------------- function crc32(buf) { let c, crc = 0xffffffff; for (let n = 0; n < buf.length; n++) { c = (crc ^ buf[n]) & 0xff; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; crc = (crc >>> 8) ^ c; } return (crc ^ 0xffffffff) >>> 0; } function png(w, h) { const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const td = Buffer.concat([Buffer.from(type), data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td)); return Buffer.concat([len, td, crc]); }; const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2; const raw = Buffer.alloc((w * 3 + 1) * h); for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) raw.set([217, 119, 87], y * (w * 3 + 1) + 1 + x * 3); return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw)), chunk("IEND", Buffer.alloc(0))]); } const PAGE = ` Connector test

Connector test page



`;

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "theseus-cc-e2e-"));
const profile = path.join(tmp, "profile");
const uploads = path.join(tmp, "uploads");
const outside = path.join(tmp, "outside");
for (const d of [profile, uploads, outside, path.join(profile, "extensions-data")]) fs.mkdirSync(d, { recursive: true });
const PNG = png(8, 6);
fs.writeFileSync(path.join(uploads, "test.png"), PNG);
fs.writeFileSync(path.join(outside, "secret.png"), PNG);

const site = http.createServer((req, res) => { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(PAGE); });
await new Promise((r) => site.listen(0, "127.0.0.1", r));
const siteOrigin = `http://127.0.0.1:${site.address().port}`;

// The profile: every other built-in extension off (a quicker, quieter
// start), the connector on, its site and upload folder already approved.
const bundled = fs.readdirSync(path.join(appDir, "bundled-addons"), { withFileTypes: true })
  .filter((d) => d.isDirectory())
  .map((d) => { try { return JSON.parse(fs.readFileSync(path.join(appDir, "bundled-addons", d.name, "addon.json"), "utf8")).id; } catch { return null; } })
  .filter(Boolean);
fs.writeFileSync(path.join(profile, "settings.json"), JSON.stringify({ disabledAddons: bundled.filter((id) => id !== "claude-connector"), addonsDefaultOffSeen: ["claude-connector"] }));
const TOKEN = crypto.randomBytes(32).toString("base64url");
fs.writeFileSync(path.join(profile, "extensions-data", "claude-connector.json"), JSON.stringify({ token: TOKEN, accept: true, allowedOrigins: [siteOrigin], uploadDirs: [uploads] }));

// ---- launch ------------------------------------------------------------------
const electron = require("electron");
const out = [];
const env = { ...process.env, THESEUS_USER_DATA: profile, THESEUS_NO_UPDATE_CHECK: "1" };
delete env.ELECTRON_RUN_AS_NODE;
const child = spawn(electron, [appDir], { env, stdio: ["ignore", "pipe", "pipe"] });
child.stdout.on("data", (d) => out.push(String(d)));
child.stderr.on("data", (d) => out.push(String(d)));
let exited = false;
child.on("exit", () => { exited = true; });

function shutdown() {
  if (!KEEP) {
    try { if (process.platform === "win32") execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); else child.kill(); } catch {}
  }
  site.close();
}

const endpointFile = path.join(profile, "extensions-data", "claude-connector", "endpoint.json");
let url = null;
for (let i = 0; i < 240 && !url && !exited; i++) {
  try { url = JSON.parse(fs.readFileSync(endpointFile, "utf8")).url; } catch {}
  if (!url) await sleep(250);
}
if (!url) {
  console.log("Theseus did not start the connector. Last output:\n" + out.join("").split("\n").slice(-40).join("\n"));
  shutdown();
  process.exit(1);
}
console.log(`connector at ${url}`);

// ---- a minimal MCP client ------------------------------------------------------
let sid = null, rid = 0;
async function rpc(method, params, { token = TOKEN, headers = {} } = {}) {
  const h = { "content-type": "application/json", accept: "application/json, text/event-stream", ...headers };
  if (token) h.authorization = `Bearer ${token}`;
  if (sid) h["mcp-session-id"] = sid;
  const isNote = method.startsWith("notifications/");
  const res = await fetch(url, { method: "POST", headers: h, body: JSON.stringify({ jsonrpc: "2.0", method, ...(params ? { params } : {}), ...(isNote ? {} : { id: ++rid }) }) });
  const s = res.headers.get("mcp-session-id");
  if (s) sid = s;
  const text = await res.text();
  return { status: res.status, json: text ? (() => { try { return JSON.parse(text); } catch { return null; } })() : null };
}
async function tool(name, args = {}) {
  const r = await rpc("tools/call", { name, arguments: args });
  if (r.json?.error) return { error: r.json.error.message };
  const res = r.json.result;
  const txt = (res.content || []).filter((c) => c.type === "text").map((c) => c.text).join("\n");
  return res.isError ? { error: txt } : { text: txt, content: res.content };
}
async function report(tabId) {
  const r = await tool("read_page_text", { tab_id: tabId });
  const m = /REPORT(\{.*\})END/s.exec(r.text || "");
  return m ? JSON.parse(m[1]) : null;
}
function clipboardText() {
  if (process.platform !== "win32") return null;
  try { return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard -Raw"], { encoding: "utf8" }); } catch { return null; }
}

try {
  console.log("auth");
  const noTok = await rpc("initialize", { protocolVersion: "2025-06-18", clientInfo: { name: "test" } }, { token: null });
  check("no token is refused (401)", noTok.status === 401, `status ${noTok.status}`);
  const browser = await rpc("initialize", { protocolVersion: "2025-06-18" }, { headers: { origin: "https://evil.example" } });
  check("a browser Origin is refused (403)", browser.status === 403, `status ${browser.status}`);

  console.log("session");
  const init = await rpc("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "connector-e2e", version: "1" } });
  check("initialize", init.status === 200 && init.json?.result?.serverInfo?.name === "theseus" && !!sid, JSON.stringify(init.json));
  await rpc("notifications/initialized");
  const list = await rpc("tools/list");
  const names = (list.json?.result?.tools || []).map((t) => t.name);
  const required = ["list_tabs", "open_tab", "navigate", "close_tab", "screenshot", "read_page_text", "snapshot", "click", "type", "paste_html", "paste_text", "upload_file", "wait_for"];
  check("every required tool is listed", required.every((n) => names.includes(n)), `missing: ${required.filter((n) => !names.includes(n))}`);
  check("evaluate is not offered while it is off", !names.includes("evaluate"));

  console.log("open + snapshot");
  const opened = await tool("open_tab", { url: siteOrigin + "/" });
  const tabId = opened.text ? JSON.parse(opened.text).tab_id : null;
  check("open_tab", Number.isInteger(tabId), opened.error || opened.text);
  const tabs = await tool("list_tabs");
  check("list_tabs shows the tab with its URL", (tabs.text || "").includes(siteOrigin), tabs.text);
  const waited = await tool("wait_for", { tab_id: tabId, selector: "#editor" });
  check("wait_for selector", !waited.error, waited.error);
  const snap = await tool("snapshot", { tab_id: tabId, interactive_only: true });
  const refOf = (label) => { const m = new RegExp(`"${label}"[^\\n]*\\[ref=(e\\d+)\\]`).exec(snap.text || ""); return m && m[1]; };
  const refs = { editor: refOf("Story editor"), title: refOf("Title"), pw: refOf("Password"), file: refOf("Image upload") };
  check("snapshot has refs for editor, inputs and file input", Object.values(refs).every(Boolean), `${JSON.stringify(refs)}\n${snap.text || snap.error}`);

  console.log("type");
  const c1 = await tool("click", { tab_id: tabId, ref: refs.editor });
  check("click the editor by ref", !c1.error, c1.error);
  const t1 = await tool("type", { tab_id: tabId, text: "Hello world\nSecond line" });
  check("type", !t1.error, t1.error);
  await sleep(200);
  let r = await report(tabId);
  check("typed text landed in the editor, Enter made a new line", /Hello world\n+Second line/.test(r?.editorText || ""), JSON.stringify(r?.editorText));
  check("the editor saw real input events (beforeinput insertText)", (r?.beforeinput?.insertText || 0) > 0, JSON.stringify(r?.beforeinput));
  check("Enter arrived as a trusted key press", r?.enterKeys === 1, `enterKeys ${r?.enterKeys}`);

  console.log("paste_html");
  const clipBefore = clipboardText();
  await tool("press_key", { tab_id: tabId, key: "End" });
  await tool("press_key", { tab_id: tabId, key: "Enter" });
  const html = '

A heading from Claude

Read the full story here.

'; const p1 = await tool("paste_html", { tab_id: tabId, html }); check("paste_html", !p1.error, p1.error); await sleep(300); r = await report(tabId); check("the page got a trusted paste event carrying text/html", r?.paste?.isTrusted === true && r.paste.types.includes("text/html") && r.paste.htmlLength > 0, JSON.stringify(r?.paste)); check("the heading arrived as

", /]*>A heading from Claude<\/h2>/.test(r?.editorHtml || ""), r?.editorHtml); check("the link arrived with its href", /]*href="https:\/\/example\.com\/story"[^>]*>the full story<\/a>/.test(r?.editorHtml || ""), r?.editorHtml); check("earlier text is still there", (r?.editorText || "").startsWith("Hello world"), JSON.stringify(r?.editorText)); const clipAfter = clipboardText(); if (clipBefore !== null) check("the user's clipboard text is back after the paste", clipAfter === clipBefore, `before ${JSON.stringify(clipBefore?.slice(0, 40))} after ${JSON.stringify(clipAfter?.slice(0, 40))}`); const p2 = await tool("paste_text", { tab_id: tabId, text: " Plain tail." }); await sleep(250); r = await report(tabId); check("paste_text", !p2.error && (r?.editorText || "").includes("Plain tail."), p2.error || JSON.stringify(r?.editorText)); console.log("inputs and password fields"); const t2 = await tool("type", { tab_id: tabId, ref: refs.title, text: "My title" }); check("type into a text input by ref", !t2.error, t2.error); const pw1 = await tool("type", { tab_id: tabId, ref: refs.pw, text: "hunter2" }); check("type into a password field by ref is refused", /password/i.test(pw1.error || ""), pw1.error || pw1.text); const pw2 = await tool("click", { tab_id: tabId, ref: refs.pw }); check("click on a password field by ref is refused", /password/i.test(pw2.error || ""), pw2.error || pw2.text); await tool("click", { tab_id: tabId, ref: refs.title }); await tool("press_key", { tab_id: tabId, key: "Tab" }); // focus moves to the password field const pw3 = await tool("type", { tab_id: tabId, text: "hunter2" }); check("type while focus sits in a password field is refused", /password/i.test(pw3.error || ""), pw3.error || pw3.text); const pw4 = await tool("paste_text", { tab_id: tabId, text: "hunter2" }); check("paste into a password field is refused", /password/i.test(pw4.error || ""), pw4.error || pw4.text); await sleep(150); r = await report(tabId); check("the title input has the text, the password field stayed empty", r?.title === "My title" && r?.pwLength === 0, `title ${JSON.stringify(r?.title)} pw ${r?.pwLength}`); console.log("upload_file"); const u1 = await tool("upload_file", { tab_id: tabId, ref: refs.file, path: path.join(uploads, "test.png") }); check("upload a PNG from the allowed folder", !u1.error, u1.error); await sleep(300); r = await report(tabId); check("the page received test.png as a real PNG", r?.file?.name === "test.png" && r.file.size === PNG.length && r.file.isPng, JSON.stringify(r?.file)); const u2 = await tool("upload_file", { tab_id: tabId, ref: refs.file, path: path.join(outside, "secret.png") }); check("a file outside the allowed folder is refused", /outside/i.test(u2.error || ""), u2.error || u2.text); const u3 = await tool("upload_file", { tab_id: tabId, ref: refs.title, path: path.join(uploads, "test.png") }); check("upload onto a non-file input is refused", /not a file input/i.test(u3.error || ""), u3.error || u3.text); console.log("reading"); const txt = await tool("read_page_text", { tab_id: tabId }); check("read_page_text", (txt.text || "").includes("Connector test page"), txt.error); const shot = await tool("screenshot", { tab_id: tabId }); const img = (shot.content || []).find((c) => c.type === "image"); const imgBuf = img ? Buffer.from(img.data, "base64") : Buffer.alloc(0); check("screenshot is a PNG", imgBuf.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47])), shot.error); if (SHOT && img) { fs.writeFileSync(SHOT, imgBuf); console.log(` saved ${SHOT}`); } const w2 = await tool("wait_for", { tab_id: tabId, text: "this text never appears", timeout_ms: 400 }); check("wait_for times out cleanly", /timed out/.test(w2.error || ""), w2.error || w2.text); const ev = await tool("evaluate", { tab_id: tabId, expression: "1+1" }); check("evaluate is refused while it is off", !!ev.error, ev.text); console.log("log"); const logFile = path.join(profile, "extensions-data", "claude-connector", "actions.log"); const entries = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8").trim().split("\n").map((l) => JSON.parse(l)) : []; const tools = new Set(entries.map((e) => e.tool)); check("every action is in the log with a timestamp", ["open_tab", "type", "paste_html", "upload_file", "click"].every((t) => tools.has(t)) && entries.every((e) => /^\d{4}-\d\d-\d\dT/.test(e.ts)), [...tools].join(",")); check("refusals are logged as failures", entries.some((e) => e.tool === "type" && !e.ok && /password/i.test(e.error || ""))); check("typed secrets are not logged in full", !entries.some((e) => String(e.args || "").includes("hunter2hunter2"))); if (!KEEP) { const cl = await tool("close_tab", { tab_id: tabId }); check("close_tab", !cl.error, cl.error); } } catch (e) { failed++; console.log(` FAIL ${e?.stack || e}`); } finally { shutdown(); } if (failed) console.log("\nTheseus output (tail):\n" + out.join("").split("\n").filter((l) => /claude-connector|error|Error/i.test(l)).slice(-30).join("\n")); console.log(`\n${passed} passed, ${failed} failed${KEEP ? `\nprofile kept at ${profile}` : ""}`); if (!KEEP) { await sleep(500); try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } process.exit(failed ? 1 : 0);