Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107// E2E for the Codex bridge built into drawgent (no Node.js adapter): `drawgent serve` runs a// fake Codex CLI (scripts/fake-codex.mjs, app-server protocol) through `drawgent bridge codex`,// and a real browser checks streamed replies, permission prompts in the chat panel, Stop, and// per-turn usage.// node scripts/codex-bridge-e2e.mjs [path/to/drawgent]import { spawn, spawnSync } from "node:child_process";import fs from "node:fs";import os from "node:os";import path from "node:path";import { fileURLToPath } from "node:url";import { chromium } from "playwright-core";
const here = path.dirname(fileURLToPath(import.meta.url));const BIN = path.resolve(process.argv[2] || "./target/release/drawgent");const PORT = 7393, API = `http://127.0.0.1:${PORT}`;const ws = fs.mkdtempSync(path.join(os.tmpdir(), "dg-xbridge-"));const LOG = path.join(os.tmpdir(), `dg-fake-codex-${process.pid}.jsonl`);const FAKE = path.join(here, "fake-codex.mjs");fs.chmodSync(FAKE, 0o755);const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const checks = [];const ok = (name, cond, extra = "") => { checks.push(!!cond); console.log(`${cond ? "✓" : "✗"} ${name}${extra ? ` (${extra})` : ""}`); };const entries = () => (fs.existsSync(LOG) ? fs.readFileSync(LOG, "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) : []);
// `drawgent setup` against the fake CLI (on PATH as `codex`), into a throwaway config.{ const bin = fs.mkdtempSync(path.join(os.tmpdir(), "dg-fakebin-")); fs.symlinkSync(FAKE, path.join(bin, "codex")); const cfg = path.join(bin, "config.toml"); const chrome = process.env.DRAWGENT_CHROME || "/usr/bin/google-chrome"; const r = spawnSync(BIN, ["setup", "codex", "--chrome", chrome, "--skip-mcp"], { encoding: "utf8", timeout: 60000, env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, DRAWGENT_CONFIG: cfg, XDG_CACHE_HOME: bin }, }); const conf = fs.existsSync(cfg) ? fs.readFileSync(cfg, "utf8") : ""; ok("setup codex succeeds with the built-in bridge, no Node.js", r.status === 0 && conf.includes('"@drawgent"') && conf.includes('"bridge"') && !/node/.test(conf.replace(/fake-\w+\.mjs|node_modules/g, "")), (r.stderr || "").split("\n").filter((l) => /✓|!|Error|bridge/.test(l)).slice(-4).join(" / "));}const srv = spawn(BIN, ["serve", "--port", String(PORT), "--workdir", ws, "--data", "-", "--agent", `codex=@drawgent bridge codex --cli ${FAKE}`], { cwd: ws, env: { ...process.env, FAKE_CODEX_LOG: LOG }, stdio: ["ignore", "ignore", "inherit"],});let browser;try { let health; for (let i = 0; i < 60; i++) { health = await fetch(`${API}/api/health`).then((r) => r.json()).catch(() => null); if (health?.agents?.[0]?.state === "idle") break; await sleep(250); } ok("the built-in bridge starts the agent", health?.agents?.[0]?.state === "idle" && health.agents[0].detail === "Codex", JSON.stringify(health?.agents)); for (let i = 0; i < 40 && !entries().some((e) => e.in?.method === "thread/start"); i++) await sleep(100); const start = entries().find((e) => e.in?.method === "thread/start")?.in.params || {}; const mcp = start.config?.["mcp_servers.drawgent"]; ok("a Codex thread with drawgent's tools", mcp?.args?.includes("mcp") && start.approvalPolicy === "on-request" && entries().some((e) => e.in?.method === "initialized"), JSON.stringify(start).slice(0, 200));
browser = await chromium.launch({ executablePath: process.env.DRAWGENT_CHROME || "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox"] }); const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); await page.goto(API); await page.waitForFunction(() => window.drawgentEditor); await page.click("text=💬 Chat"); const say = async (t) => { await page.fill(".dg-panel textarea", t); await page.keyboard.press("Enter"); };
// A turn streams its reply and shows its usage. await say("hello there"); await page.waitForSelector(".dg-usage", { timeout: 15000 }); const reply = await page.$$eval(".dg-agent", (l) => l.map((x) => x.textContent).join(" | ")); ok("reply streams into the chat", reply.includes("Fake Codex got: You are a diagramming agent"), reply.slice(0, 120)); const first = entries().find((e) => e.in?.method === "turn/start")?.in.params.input[0].text || ""; ok("the first message carries drawgent's protocol", first.includes("live Excalidraw canvas") && first.includes("hello there")); const usage = await page.$eval(".dg-usage", (x) => x.textContent); ok("usage under the turn: tokens and context", /1\.5k in \(1\.2k cached\) · 150 out · context 1\.6k \/ 258\.4k/.test(usage), usage);
// A command that needs approval asks in the chat panel; Allow reaches Codex. await say("please RUN the tests"); await page.waitForSelector(".dg-perm button", { timeout: 15000 }); const opts = await page.$$eval(".dg-perm button", (l) => l.map((b) => b.textContent)); ok("permission prompt with Always / Allow / Deny", opts.join("|") === "Always allow|Allow|Deny", opts.join(" | ")); await page.click(".dg-perm button:has-text('Allow'):not(:has-text('Always'))"); await page.waitForFunction(() => document.querySelectorAll(".dg-usage").length >= 2, null, { timeout: 15000 }); const perm = entries().find((e) => e.approval)?.approval.result; ok("Allow is sent back as accept", perm?.decision === "accept", JSON.stringify(perm)); const tools = await page.$$eval(".dg-tool", (l) => l.map((x) => x.textContent)); ok("the tool call shows with its title and outcome", tools.some((t) => t.includes("Run: make test") && !t.includes("failed")), tools.join(" | "));
// Stop interrupts a running turn. await say("SLOW count"); await page.waitForSelector(".dg-stop", { timeout: 10000 }); await sleep(600); await page.click(".dg-stop"); await page.waitForFunction(() => document.querySelectorAll(".dg-usage").length >= 3 || [...document.querySelectorAll(".dg-sys")].some((x) => x.textContent.includes("cancelled")), null, { timeout: 15000 }); const sys = await page.$$eval(".dg-sys", (l) => l.map((x) => x.textContent)); ok("Stop cancels the turn", sys.some((t) => t.includes("cancelled")) && entries().some((e) => e.in?.method === "turn/interrupt"), sys.join(" | ")); await sleep(500); const h2 = await fetch(`${API}/api/health`).then((r) => r.json()); ok("the agent is idle again", h2.agents[0].state === "idle", h2.agents[0].state);} catch (e) { console.error(e); checks.push(false);} finally { await browser?.close(); srv.kill();}const failed = checks.filter((c) => !c).length;console.log(failed ? `CODEX BRIDGE E2E FAILED (${failed})` : "CODEX BRIDGE E2E OK");process.exit(failed ? 1 : 0);