Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181// E2E for the token-saving tools: check_layout (geometry lint, also attached to edit replies),// add_graph (server-side auto-layout), smaller screenshots, incremental get_scene through a real// `drawgent mcp` bridge, and per-turn token usage in the chat panel (fake ACP agent).// node scripts/efficiency-e2e.mjs [path/to/drawgent]import { spawn } from "node:child_process";import fs from "node:fs";import os from "node:os";import path from "node:path";import readline from "node:readline";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 = 7396, API = `http://127.0.0.1:${PORT}`;const ws = fs.mkdtempSync(path.join(os.tmpdir(), "dg-eff-"));const OUT = process.env.EFF_OUT || os.tmpdir();const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const api = (p, body, method = body ? "POST" : "GET") => fetch(API + p, { method, headers: { "content-type": "application/json" }, body: body && JSON.stringify(body) }).then(async (r) => { const t = await r.text(); if (!r.ok) throw new Error(`${method} ${p}: ${r.status} ${t}`); return JSON.parse(t); });const checks = [];const ok = (name, cond, extra = "") => { checks.push(!!cond); console.log(`${cond ? "✓" : "✗"} ${name}${extra ? ` (${extra})` : ""}`); };const pngSize = (buf) => [buf.readUInt32BE(16), buf.readUInt32BE(20)];
/** Minimal MCP client over stdio (newline-delimited JSON-RPC). */function mcp() { const p = spawn(BIN, ["mcp", "--url", API], { stdio: ["pipe", "pipe", "inherit"] }); const waiting = new Map(); let id = 0; readline.createInterface({ input: p.stdout }).on("line", (l) => { const m = JSON.parse(l); if (m.id != null && waiting.has(m.id)) (waiting.get(m.id)(m), waiting.delete(m.id)); }); const req = (method, params) => new Promise((res) => { waiting.set(++id, res); p.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"); }); return { async init() { await req("initialize", { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "e2e", version: "1" } }); p.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"); }, async tools() { return (await req("tools/list", {})).result.tools.map((t) => t.name); }, async call(name, args = {}) { const r = await req("tools/call", { name, arguments: args }); const c = r.result?.content?.[0]; if (r.error || r.result?.isError) throw new Error(`${name}: ${JSON.stringify(r.error || c)}`); return c.type === "text" ? { json: JSON.parse(c.text), chars: c.text.length } : c; }, kill: () => p.kill(), };}
const fake = `fake=node ${path.join(here, "fake-acp-agent.mjs")}`;const srv = spawn(BIN, ["serve", "--port", String(PORT), "--workdir", ws, "--data", "-", "--agent", fake], { cwd: ws, stdio: ["ignore", "ignore", "inherit"] });let browser, bridge;try { for (let i = 0; i < 60; i++) { const h = await api("/api/health").catch(() => null); if (h?.agents?.[0]?.state === "idle") break; await sleep(250); }
// ---- check_layout: problems reported with the edit, fixes apply cleanly. const bad = await api("/api/elements", { elements: [ { type: "rectangle", id: "a", x: 0, y: 0, width: 180, height: 80, label: { text: "Gateway" } }, { type: "rectangle", id: "b", x: 120, y: 40, width: 180, height: 80, label: { text: "Auth" } }, { type: "rectangle", id: "mid", x: 520, y: 300, width: 160, height: 70, label: { text: "Queue" } }, { type: "rectangle", id: "c", x: 900, y: 0, width: 160, height: 70, label: { text: "Worker" } }, { type: "rectangle", id: "d", x: 900, y: 600, width: 160, height: 70, label: { text: "Store" } }, { type: "arrow", id: "cross", x: 300, y: 335, points: [[0, 0], [600, 0]] }, { type: "text", id: "tiny", x: 0, y: 200, text: "footnote", fontSize: 10 }, ], }); const kinds = (r) => (r.layout?.issues || r.issues || []).map((i) => i.kind); ok("add_elements reply carries layout issues", kinds(bad).includes("overlap") && kinds(bad).includes("arrow-through-shape") && kinds(bad).includes("small-font"), kinds(bad).join(",")); const fix = bad.layout.issues.find((i) => i.kind === "overlap").fix; const fixed = await api("/api/elements", { updates: [fix, { id: "tiny", fontSize: 16 }, { id: "cross", dy: 200 }] }, "PATCH"); ok("applying the suggested fixes clears them", !kinds(fixed).includes("overlap") && !kinds(fixed).includes("small-font"), kinds(fixed).join(",") || "clean"); await api("/api/elements", { updates: [{ id: "mid", width: 60, height: 30 }] }, "PATCH"); const lint = await api("/api/lint?ids=mid"); ok("squeezed label found with a size fix", lint.issues.some((i) => (i.kind === "label-overflow" || i.kind === "label-broken") && i.fix?.width > 60), JSON.stringify(lint.issues.map((i) => i.kind))); ok("lint report is small", JSON.stringify(lint).length < 1200, `${JSON.stringify(lint).length} chars`); await api("/api/clear", {});
// ---- add_graph: placed natively, grouped in frames, clean. const g = await api("/api/graph", { title: "Checkout", nodes: [ { id: "web", label: "Web app", group: "client" }, { id: "mobile", label: "Mobile app", group: "client" }, { id: "api", label: "API gateway", role: "service", group: "backend" }, { id: "orders", label: "Orders", role: "service", group: "backend" }, { id: "pay", label: "Payments", role: "external" }, { id: "db", label: "Orders DB", role: "storage", group: "backend" }, { id: "audit", label: "Audit log", role: "neutral" }, ], edges: [ { from: "web", to: "api" }, { from: "mobile", to: "api" }, { from: "api", to: "orders" }, { from: "orders", to: "db" }, { from: "orders", to: "pay", label: "charge" }, { from: "api", to: "audit", style: "dashed" }, { from: "web", to: "db" }, ], groups: [{ id: "client", label: "Clients" }, { id: "backend", label: "Backend" }], }); const box = (id) => g.boxes[id]; ok("graph placed every node and frame", ["web", "mobile", "api", "orders", "pay", "db", "audit", "client", "backend"].every((i) => box(i)), Object.keys(g.boxes).join(",")); ok("layered left to right", box("web")[0] < box("api")[0] && box("api")[0] < box("orders")[0] && box("orders")[0] < box("db")[0]); const inFrame = (n, f) => box(n)[0] >= box(f)[0] && box(n)[1] >= box(f)[1] && box(n)[0] + box(n)[2] <= box(f)[0] + box(f)[2] && box(n)[1] + box(n)[3] <= box(f)[1] + box(f)[3]; ok("group members sit inside their frame", ["web", "mobile"].every((n) => inFrame(n, "client")) && ["api", "orders", "db"].every((n) => inFrame(n, "backend"))); const glint = await api("/api/lint"); ok("auto-layout passes check_layout", glint.errors === 0, JSON.stringify(glint.issues.filter((i) => i.severity === "error"))); const scene = await api("/api/scene/raw"); const frames = scene.elements.filter((e) => e.type === "frame"); ok("frames hold their children", frames.length === 2 && scene.elements.filter((e) => e.frameId).length >= 5, `${frames.length} frames`); const longEdge = scene.elements.find((e) => e.id === "web-db"); ok("long edge bends around layers", longEdge?.points.length > 2, `${longEdge?.points.length} points`);
// ---- screenshots: 1000 px by default, bigger on request, small when zoomed. const shot = async (q) => pngSize(Buffer.from(await (await fetch(`${API}/api/screenshot${q}`)).arrayBuffer())); const def = await shot(""); fs.writeFileSync(path.join(OUT, "efficiency-graph.png"), Buffer.from(await (await fetch(`${API}/api/screenshot?max=1600`)).arrayBuffer())); ok("default screenshot ≤ 1000 px", Math.max(...def) <= 1000 && Math.max(...def) > 600, def.join("×")); ok("max=1600 gives a bigger image", Math.max(...(await shot("?max=1600"))) > 1000); const zoom = await shot("?ids=pay&padding=40"); ok("focused screenshot is small", zoom[0] * zoom[1] < def[0] * def[1] / 2, zoom.join("×"));
// ---- incremental get_scene through drawgent mcp. bridge = mcp(); await bridge.init(); const tools = await bridge.tools(); ok("mcp lists check_layout and add_graph", tools.includes("check_layout") && tools.includes("add_graph")); const first = await bridge.call("get_scene"); ok("first get_scene is full", first.json.elements?.length >= 14 && first.json.note, `${first.json.elements?.length} elements, ${first.chars} chars`); const again = await bridge.call("get_scene"); ok("unchanged scene costs almost nothing", again.json.changes && again.chars < 400, `${again.chars} chars vs ${first.chars}`); // Own edit through the bridge: not echoed back. Human edit (REST, not the bridge): reported. await bridge.call("update_elements", { updates: [{ id: "audit", label: "Audit trail" }] }); await api("/api/elements", { elements: [{ type: "rectangle", id: "human", x: 0, y: 900, width: 160, height: 70, label: { text: "Added by human" } }] }); await api("/api/elements", { ids: ["mobile"] }, "DELETE"); const delta = await bridge.call("get_scene"); const ids = (l) => (l || []).map((e) => e.id); ok("delta has the human's addition", ids(delta.json.added).includes("human"), JSON.stringify({ added: ids(delta.json.added), changed: ids(delta.json.changed), removed: delta.json.removed })); ok("delta has the removal", delta.json.removed?.includes("mobile")); ok("own edit not repeated", !ids(delta.json.changed).includes("audit") && !ids(delta.json.added).includes("audit")); const full = await bridge.call("get_scene", { full: true }); ok("full: true returns everything", full.json.elements?.length >= 14); const graphViaMcp = await bridge.call("add_graph", { nodes: [{ id: "x1", label: "One" }, { id: "x2", label: "Two" }], edges: [{ from: "x1", to: "x2" }], direction: "down", x: 2000, y: 0 }); ok("add_graph through mcp", graphViaMcp.json.boxes?.x1 && graphViaMcp.json.arrows?.includes("x1-x2")); const afterGraph = await bridge.call("get_scene"); ok("own graph not echoed back", !ids(afterGraph.json.added).some((i) => i.startsWith("x")), JSON.stringify(ids(afterGraph.json.added))); const cl = await bridge.call("check_layout", { element_ids: ["x1", "x2"] }); ok("check_layout through mcp", cl.json.ok === true && cl.chars < 300, `${cl.chars} chars`);
// ---- token usage per turn in the chat panel. 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: 1500, height: 900 } }); await page.goto(API); await page.waitForFunction(() => window.drawgentEditor); await page.click("text=💬 Chat"); for (const t of ["first question", "second question"]) { await page.fill(".dg-panel textarea", t); await page.keyboard.press("Enter"); await sleep(1500); } const lines = await page.$$eval(".dg-usage", (l) => l.map((x) => x.textContent)); ok("each turn shows its usage", lines.length >= 2, lines.join(" | ")); ok("tokens, context and cost", /11\.2k in \(10\.0k cached\) · 80 out · context 24\.0k \/ 200\.0k \(12%\) · \$0\.020/.test(lines.at(-1) || ""), lines.at(-1));} catch (e) { console.error(e); checks.push(false);} finally { await browser?.close(); bridge?.kill(); srv.kill();}const failed = checks.filter((c) => !c).length;console.log(failed ? `EFFICIENCY E2E FAILED (${failed})` : "EFFICIENCY E2E OK");process.exit(failed ? 1 : 0);